Files
cli/ts/mod_config/index.ts
T

1884 lines
58 KiB
TypeScript

// gitzone config - manage CLI smartconfig configuration
import * as plugins from "./mod.plugins.js";
import { ReleaseConfig } from "./classes.releaseconfig.js";
import { CommitConfig } from "./classes.commitconfig.js";
import { runFormatter, type ICheckResult } from "../mod_format/index.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
CLI_NAMESPACE,
getCliConfigValueFromData,
getSmartconfigPath,
readSmartconfigFile,
setCliConfigValueInData,
unsetCliConfigValueInData,
writeSmartconfigFile,
} from "../helpers.smartconfig.js";
import {
CURRENT_GITZONE_CLI_SCHEMA_VERSION,
migrateSmartconfigData,
} from "../helpers.smartconfigmigrations.js";
export { ReleaseConfig, CommitConfig };
const defaultCliMode: ICliMode = {
output: "human",
interactive: true,
json: false,
plain: false,
quiet: false,
yes: false,
help: false,
agent: false,
checkUpdates: true,
isTty: true,
};
/**
* Format .smartconfig.json with diff preview
* Shows diff first, asks for confirmation, then applies
*/
async function formatSmartconfigWithDiff(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
return;
}
// Check for diffs first
const checkResult = (await runFormatter("smartconfig", {
checkOnly: true,
showDiff: true,
})) as ICheckResult | void;
if (checkResult && checkResult.hasDiff) {
const shouldApply =
await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Apply formatting changes to .smartconfig.json?",
true,
);
if (shouldApply) {
await runFormatter("smartconfig", { silent: true });
}
}
}
export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const command = argvArg._?.[1];
const value = argvArg._?.[2];
if (mode.help || command === "help") {
showHelp(mode);
return;
}
// If no command provided, show interactive menu
if (!command) {
if (!mode.interactive) {
showHelp(mode);
return;
}
await handleInteractiveMenu();
return;
}
switch (command) {
case "show":
await handleShow(mode);
break;
case "add":
await handleAdd(value, mode);
break;
case "remove":
await handleRemove(value, mode);
break;
case "clear":
await handleClear(mode);
break;
case "access":
case "accessLevel":
await handleAccessLevel(value, mode);
break;
case "commit":
await handleCommit(argvArg._?.[2], argvArg._?.[3], mode);
break;
case "project":
await handleProject(mode);
break;
case "cli":
await handleCli(mode);
break;
case "release":
await handleRelease(mode);
break;
case "services":
await handleServices(mode);
break;
case "doctor":
await handleDoctor(mode);
break;
case "fix":
await handleFix(argvArg, mode);
break;
case "migrate":
await handleMigrate(value, mode);
break;
case "get":
await handleGet(value, mode);
break;
case "set":
await handleSet(value, argvArg._?.[3], mode);
break;
case "unset":
await handleUnset(value, mode);
break;
default:
plugins.logger.log("error", `Unknown command: ${command}`);
showHelp(mode);
}
};
/**
* Interactive menu for config command
*/
async function handleInteractiveMenu(): Promise<void> {
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ gitzone config - Project Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "list",
name: "action",
message: "What would you like to do?",
default: "show",
choices: [
{ name: "Show current configuration", value: "show" },
{ name: "Configure project basics", value: "project" },
{ name: "Configure CLI behavior", value: "cli" },
{ name: "Configure commit workflow", value: "commit" },
{ 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" },
{ name: "Set access level (public/private)", value: "access" },
{ name: "Migrate smartconfig schema", value: "migrate" },
{ name: "Show help", value: "help" },
],
});
const action = (response as any).value;
switch (action) {
case "show":
await handleShow(defaultCliMode);
break;
case "project":
await handleProject(defaultCliMode);
break;
case "cli":
await handleCli(defaultCliMode);
break;
case "release":
await handleRelease(defaultCliMode);
break;
case "add":
await handleAdd(undefined, defaultCliMode);
break;
case "remove":
await handleRemove(undefined, defaultCliMode);
break;
case "clear":
await handleClear(defaultCliMode);
break;
case "access":
await handleAccessLevel(undefined, defaultCliMode);
break;
case "migrate":
await handleMigrate(undefined, defaultCliMode);
break;
case "commit":
await handleCommit(undefined, undefined, defaultCliMode);
break;
case "services":
await handleServices(defaultCliMode);
break;
case "doctor":
await handleDoctor(defaultCliMode);
break;
case "fix":
await handleFix({ _: ["config", "fix"] }, defaultCliMode);
break;
case "help":
showHelp();
break;
}
}
/**
* Show current CLI project configuration
*/
async function handleShow(mode: ICliMode): Promise<void> {
const smartconfigData = await readSmartconfigFile();
const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
if (mode.json) {
printJson(cliConfig);
return;
}
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ gitzone config - Project Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
if (Object.keys(cliConfig).length === 0) {
plugins.logger.log("warn", `No ${CLI_NAMESPACE} configuration found.`);
console.log(" Run `gitzone config project` to create project basics.");
console.log("");
return;
}
printConfigSection("Project", [
["schemaVersion", formatValue(cliConfig.schemaVersion)],
["projectType", formatValue(cliConfig.projectType)],
["repository", formatRepository(cliConfig.module)],
["description", formatValue(cliConfig.module?.description)],
["npm package", formatValue(cliConfig.module?.npmPackagename || cliConfig.module?.npmPackageName)],
["license", formatValue(cliConfig.module?.license)],
["keywords", formatList(cliConfig.module?.keywords)],
]);
printConfigSection("CLI Behavior", [
["interactive", formatValue(cliConfig.cli?.interactive)],
["output", formatValue(cliConfig.cli?.output)],
["checkUpdates", formatValue(cliConfig.cli?.checkUpdates)],
]);
printConfigSection("Commit Workflow", [
["confirmation", formatValue(cliConfig.commit?.confirmation)],
["steps", formatList(cliConfig.commit?.steps)],
["test command", formatValue(cliConfig.commit?.test?.command)],
["build command", formatValue(cliConfig.commit?.build?.command)],
["push remote", formatValue(cliConfig.commit?.push?.remote)],
["push followTags", formatValue(cliConfig.commit?.push?.followTags)],
]);
const release = cliConfig.release || {};
const targets = release.targets || {};
printConfigSection("Release Workflow", [
["confirmation", formatValue(release.confirmation)],
["require clean tree", formatValue(release.preflight?.requireCleanTree)],
["run tests", formatValue(release.preflight?.test)],
["run build", formatValue(release.preflight?.build)],
["test command", formatValue(release.preflight?.testCommand)],
["build command", formatValue(release.preflight?.buildCommand)],
]);
printConfigSection("Release Targets", [
["git", formatTarget(targets.git?.enabled, targets.git)],
["npm", formatTarget(targets.npm?.enabled, targets.npm)],
["docker", formatTarget(targets.docker?.enabled, targets.docker)],
]);
console.log("Run `gitzone config doctor` to validate this configuration.");
console.log("");
}
/**
* Add an npm target registry URL
*/
async function handleAdd(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "input",
name: "registryUrl",
message: "Enter npm target registry URL:",
default: "https://registry.npmjs.org",
validate: (input: string) => {
return !!(input && input.trim() !== "");
},
});
url = (response as any).value;
}
const config = await ReleaseConfig.fromCwd();
const added = config.addRegistry(url!);
if (added) {
await config.save();
if (mode.json) {
printJson({
ok: true,
action: "add",
registry: url,
registries: config.getRegistries(),
});
return;
}
plugins.logger.log("success", `Added npm target registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log("warn", `Registry already exists: ${url}`);
}
}
/**
* Remove an npm target registry URL
*/
async function handleRemove(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await ReleaseConfig.fromCwd();
const registries = config.getRegistries();
if (registries.length === 0) {
plugins.logger.log("warn", "No npm target registries configured to remove.");
return;
}
if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode - show list to select from
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "list",
name: "registryUrl",
message: "Select npm target registry to remove:",
choices: registries,
default: registries[0],
});
url = (response as any).value;
}
const removed = config.removeRegistry(url!);
if (removed) {
await config.save();
if (mode.json) {
printJson({
ok: true,
action: "remove",
registry: url,
registries: config.getRegistries(),
});
return;
}
plugins.logger.log("success", `Removed npm target registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log("warn", `Registry not found: ${url}`);
}
}
/**
* Clear all npm target registries
*/
async function handleClear(mode: ICliMode): Promise<void> {
const config = await ReleaseConfig.fromCwd();
if (!config.hasRegistries()) {
plugins.logger.log("info", "No npm target registries to clear.");
return;
}
// Confirm before clearing
const confirmed = mode.interactive
? await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Clear all configured npm target registries?",
false,
)
: true;
if (confirmed) {
config.clearRegistries();
await config.save();
if (mode.json) {
printJson({ ok: true, action: "clear", registries: [] });
return;
}
plugins.logger.log("success", "All npm target registries cleared.");
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log("info", "Operation cancelled.");
}
}
/**
* Set or toggle access level
*/
async function handleAccessLevel(
level: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await ReleaseConfig.fromCwd();
const currentLevel = config.getAccessLevel();
if (!level) {
if (!mode.interactive) {
throw new Error("Access level is required in non-interactive mode");
}
// Interactive mode - toggle or ask
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "list",
name: "accessLevel",
message: "Select npm access level for publishing:",
choices: ["public", "private"],
default: currentLevel,
});
level = (response as any).value;
}
// Validate the level
if (level !== "public" && level !== "private") {
plugins.logger.log(
"error",
`Invalid access level: ${level}. Must be 'public' or 'private'.`,
);
return;
}
if (level === currentLevel) {
plugins.logger.log("info", `Access level is already set to: ${level}`);
return;
}
config.setAccessLevel(level as "public" | "private");
await config.save();
if (mode.json) {
printJson({ ok: true, action: "access", accessLevel: level });
return;
}
plugins.logger.log("success", `Access level set to: ${level}`);
await formatSmartconfigWithDiff(mode);
}
/**
* Handle commit configuration
*/
async function handleCommit(
setting: string | undefined,
value: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await CommitConfig.fromCwd();
// No setting = interactive mode
if (!setting) {
if (!mode.interactive) {
throw new Error("Commit setting is required in non-interactive mode");
}
await handleCommitInteractive(config);
return;
}
// Direct setting
switch (setting) {
case "alwaysTest":
await handleCommitSetting(config, "alwaysTest", value, mode);
break;
case "alwaysBuild":
await handleCommitSetting(config, "alwaysBuild", value, mode);
break;
default:
plugins.logger.log("error", `Unknown commit setting: ${setting}`);
showCommitHelp();
}
}
/**
* Interactive commit configuration
*/
async function handleCommitInteractive(config: CommitConfig): Promise<void> {
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ Commit Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "checkbox",
name: "commitOptions",
message: "Select commit options to enable:",
choices: [
{ name: "Always run tests before commit (-t)", value: "alwaysTest" },
{ name: "Always build after commit (-b)", value: "alwaysBuild" },
],
default: [
...(config.getAlwaysTest() ? ["alwaysTest"] : []),
...(config.getAlwaysBuild() ? ["alwaysBuild"] : []),
],
});
const selected = (response as any).value || [];
config.setAlwaysTest(selected.includes("alwaysTest"));
config.setAlwaysBuild(selected.includes("alwaysBuild"));
syncCommitStepsFromBooleans(config);
await config.save();
plugins.logger.log("success", "Commit configuration updated");
await formatSmartconfigWithDiff(defaultCliMode);
}
/**
* Set a specific commit setting
*/
async function handleCommitSetting(
config: CommitConfig,
setting: string,
value: string | undefined,
mode: ICliMode,
): Promise<void> {
// Parse boolean value
const boolValue = value === "true" || value === "1" || value === "on";
if (setting === "alwaysTest") {
config.setAlwaysTest(boolValue);
} else if (setting === "alwaysBuild") {
config.setAlwaysBuild(boolValue);
}
syncCommitStepsFromBooleans(config);
await config.save();
if (mode.json) {
printJson({ ok: true, action: "commit", setting, value: boolValue });
return;
}
plugins.logger.log("success", `Set ${setting} to ${boolValue}`);
await formatSmartconfigWithDiff(mode);
}
function syncCommitStepsFromBooleans(config: CommitConfig): void {
config.setSteps([
"analyze",
...(config.getAlwaysTest() ? ["test"] : []),
...(config.getAlwaysBuild() ? ["build"] : []),
"changelog",
"commit",
]);
}
/**
* Show help for commit subcommand
*/
function showCommitHelp(): void {
console.log("");
console.log("Usage: gitzone config commit [setting] [value]");
console.log("");
console.log("Settings:");
console.log(" alwaysTest [true|false] Always run tests before commit");
console.log(" alwaysBuild [true|false] Always build after commit");
console.log("");
console.log("Examples:");
console.log(" gitzone config commit # Interactive mode");
console.log(" gitzone config commit alwaysTest true");
console.log(" gitzone config commit alwaysBuild false");
console.log("");
}
async function handleProject(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
throw new Error("Project configuration requires interactive mode. Use `gitzone config set` for automation.");
}
const smartconfigData = await readSmartconfigFile();
const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
const moduleConfig = cliConfig.module || {};
const packageJson = await readPackageJson();
const interactInstance = new plugins.smartinteract.SmartInteract();
const projectType = await askValue<string>(interactInstance, {
type: "list",
name: "projectType",
message: "What kind of project is this?",
choices: ["npm", "service", "wcc", "website"],
default: cliConfig.projectType || "npm",
});
const githost = await askValue<string>(interactInstance, {
type: "input",
name: "githost",
message: "Git host:",
default: moduleConfig.githost || "code.foss.global",
});
const gitscope = await askValue<string>(interactInstance, {
type: "input",
name: "gitscope",
message: "Git scope/owner:",
default: moduleConfig.gitscope || "git.zone",
});
const gitrepo = await askValue<string>(interactInstance, {
type: "input",
name: "gitrepo",
message: "Git repository name:",
default: moduleConfig.gitrepo || inferRepoName(packageJson.name),
});
const description = await askValue<string>(interactInstance, {
type: "input",
name: "description",
message: "Project description:",
default: moduleConfig.description || packageJson.description || "",
});
const npmPackagename = await askValue<string>(interactInstance, {
type: "input",
name: "npmPackagename",
message: "npm package name:",
default: moduleConfig.npmPackagename || moduleConfig.npmPackageName || packageJson.name || "",
});
const license = await askValue<string>(interactInstance, {
type: "input",
name: "license",
message: "License:",
default: moduleConfig.license || packageJson.license || "MIT",
});
const keywords = await askValue<string>(interactInstance, {
type: "input",
name: "keywords",
message: "Keywords (comma-separated):",
default: Array.isArray(moduleConfig.keywords)
? moduleConfig.keywords.join(", ")
: Array.isArray(packageJson.keywords)
? packageJson.keywords.join(", ")
: "",
});
setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
setCliConfigValueInData(smartconfigData, "projectType", projectType);
setCliConfigValueInData(smartconfigData, "module", {
...moduleConfig,
githost: githost.trim(),
gitscope: gitscope.trim(),
gitrepo: gitrepo.trim(),
description: description.trim(),
npmPackagename: npmPackagename.trim(),
license: license.trim(),
keywords: parseCsv(keywords),
});
await writeSmartconfigFile(smartconfigData);
plugins.logger.log("success", "Project configuration updated");
await formatSmartconfigWithDiff(mode);
}
async function handleCli(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
throw new Error("CLI behavior configuration requires interactive mode. Use `gitzone config set` for automation.");
}
const smartconfigData = await readSmartconfigFile();
const cliConfig = getCliConfigValueFromData(smartconfigData, "cli") || {};
const interactInstance = new plugins.smartinteract.SmartInteract();
const output = await askValue<string>(interactInstance, {
type: "list",
name: "output",
message: "Default output mode:",
choices: ["human", "plain", "json"],
default: cliConfig.output || "human",
});
const interactive = await askValue<boolean>(interactInstance, {
type: "confirm",
name: "interactive",
message: "Enable interactive prompts by default?",
default: cliConfig.interactive ?? true,
});
const checkUpdates = await askValue<boolean>(interactInstance, {
type: "confirm",
name: "checkUpdates",
message: "Check for gitzone updates in human mode?",
default: cliConfig.checkUpdates ?? true,
});
setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
setCliConfigValueInData(smartconfigData, "cli", {
output,
interactive,
checkUpdates,
});
await writeSmartconfigFile(smartconfigData);
plugins.logger.log("success", "CLI behavior configuration updated");
await formatSmartconfigWithDiff(mode);
}
async function handleRelease(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
throw new Error("Release configuration requires interactive mode. Use `gitzone config set` for automation.");
}
const smartconfigData = await readSmartconfigFile();
const currentRelease = getCliConfigValueFromData(smartconfigData, "release") || {};
const currentTargets = currentRelease.targets || {};
const interactInstance = new plugins.smartinteract.SmartInteract();
const confirmation = await askValue<string>(interactInstance, {
type: "list",
name: "confirmation",
message: "Release confirmation mode:",
choices: ["prompt", "auto", "plan"],
default: currentRelease.confirmation || "prompt",
});
const requireCleanTree = await askValue<boolean>(interactInstance, {
type: "confirm",
name: "requireCleanTree",
message: "Require a clean git tree before release?",
default: currentRelease.preflight?.requireCleanTree ?? true,
});
const runTests = await askValue<boolean>(interactInstance, {
type: "confirm",
name: "runTests",
message: "Run tests during release preflight?",
default: currentRelease.preflight?.test ?? false,
});
const runBuild = await askValue<boolean>(interactInstance, {
type: "confirm",
name: "runBuild",
message: "Run build during release?",
default: currentRelease.preflight?.build ?? true,
});
const testCommand = await askValue<string>(interactInstance, {
type: "input",
name: "testCommand",
message: "Release test command:",
default: currentRelease.preflight?.testCommand || "pnpm test",
});
const buildCommand = await askValue<string>(interactInstance, {
type: "input",
name: "buildCommand",
message: "Release build command:",
default: currentRelease.preflight?.buildCommand || "pnpm build",
});
const enabledTargets = await askValue<string[]>(interactInstance, {
type: "checkbox",
name: "targets",
message: "Enable release targets:",
choices: [
{ name: "git - push branch and tags", value: "git" },
{ name: "npm - publish package registries", value: "npm" },
{ name: "docker - build and push images", value: "docker" },
],
default: getDefaultEnabledTargets(currentTargets),
});
const releaseTargets: Record<string, any> = { ...currentTargets };
if (enabledTargets.includes("git")) {
releaseTargets.git = {
...(currentTargets.git || {}),
enabled: true,
remote: await askValue<string>(interactInstance, {
type: "input",
name: "gitRemote",
message: "Git remote:",
default: currentTargets.git?.remote || "origin",
}),
pushBranch: await askValue<boolean>(interactInstance, {
type: "confirm",
name: "pushBranch",
message: "Push release commit branch?",
default: currentTargets.git?.pushBranch ?? true,
}),
pushTags: await askValue<boolean>(interactInstance, {
type: "confirm",
name: "pushTags",
message: "Push release tags?",
default: currentTargets.git?.pushTags ?? true,
}),
};
} else {
releaseTargets.git = { ...(currentTargets.git || {}), enabled: false };
}
if (enabledTargets.includes("npm")) {
const registries = await askValue<string>(interactInstance, {
type: "input",
name: "npmRegistries",
message: "npm registries (comma-separated):",
default: Array.isArray(currentTargets.npm?.registries)
? currentTargets.npm.registries.join(", ")
: "https://registry.npmjs.org",
});
releaseTargets.npm = {
...(currentTargets.npm || {}),
enabled: true,
registries: parseCsv(registries).map(normalizeRegistryUrl),
accessLevel: await askValue<string>(interactInstance, {
type: "list",
name: "npmAccessLevel",
message: "npm publish access level:",
choices: ["public", "private"],
default: currentTargets.npm?.accessLevel || "public",
}),
alreadyPublished: await askValue<string>(interactInstance, {
type: "list",
name: "alreadyPublished",
message: "When a package version is already published:",
choices: ["success", "error"],
default: currentTargets.npm?.alreadyPublished || "success",
}),
};
} else {
releaseTargets.npm = { ...(currentTargets.npm || {}), enabled: false };
}
if (enabledTargets.includes("docker")) {
const images = await askValue<string>(interactInstance, {
type: "input",
name: "dockerImages",
message: "Docker image templates (comma-separated, supports {{version}}):",
default: Array.isArray(currentTargets.docker?.images)
? currentTargets.docker.images.join(", ")
: "",
});
releaseTargets.docker = {
...(currentTargets.docker || {}),
enabled: true,
images: parseCsv(images),
};
} else {
releaseTargets.docker = { ...(currentTargets.docker || {}), enabled: false };
}
setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
setCliConfigValueInData(smartconfigData, "release", {
...currentRelease,
confirmation,
preflight: {
...(currentRelease.preflight || {}),
requireCleanTree,
test: runTests,
build: runBuild,
testCommand: testCommand.trim(),
buildCommand: buildCommand.trim(),
},
targets: releaseTargets,
});
await writeSmartconfigFile(smartconfigData);
plugins.logger.log("success", "Release configuration updated");
await formatSmartconfigWithDiff(mode);
}
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();
if (!smartconfigExists) {
findings.push({
level: "warn",
message: ".smartconfig.json does not exist",
fix: "Run `gitzone config project` to create project basics.",
});
return findings;
}
let smartconfigData: Record<string, any>;
try {
smartconfigData = await readSmartconfigFile();
} catch (error) {
findings.push({
level: "error",
message: ".smartconfig.json is not valid JSON",
fix: error instanceof Error ? error.message : String(error),
});
return findings;
}
const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
if (Object.keys(cliConfig).length === 0) {
findings.push({
level: "error",
message: `${CLI_NAMESPACE} configuration is missing`,
fix: "Run `gitzone config project` or `gitzone config migrate`.",
});
}
for (const legacyNamespace of ["gitzone", "tsdoc", "npmdocker", "npmci", "szci"]) {
if (smartconfigData[legacyNamespace]) {
findings.push({
level: "warn",
message: `Legacy namespace '${legacyNamespace}' is present`,
fix: "Run `gitzone config migrate`.",
});
}
}
if (cliConfig.schemaVersion === CURRENT_GITZONE_CLI_SCHEMA_VERSION) {
findings.push({ level: "ok", message: `Schema version is current (${CURRENT_GITZONE_CLI_SCHEMA_VERSION})` });
} else {
findings.push({
level: "warn",
message: `Schema version is ${formatValue(cliConfig.schemaVersion)}, expected ${CURRENT_GITZONE_CLI_SCHEMA_VERSION}`,
fix: "Run `gitzone config migrate`.",
});
}
if (["npm", "service", "wcc", "website"].includes(cliConfig.projectType)) {
findings.push({ level: "ok", message: `Project type is ${cliConfig.projectType}` });
} else {
findings.push({
level: "warn",
message: `Project type is missing or invalid: ${formatValue(cliConfig.projectType)}`,
fix: "Run `gitzone config project`.",
});
}
await validateDetectedProjectType(cliConfig, findings);
validateCommitConfig(cliConfig.commit || {}, findings);
await validateReleaseConfig(cliConfig.release || {}, findings);
return findings;
}
/**
* Handle services configuration
*/
async function handleServices(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
throw new Error(
"Use `gitzone services config --json` or `gitzone services set ...` in non-interactive mode",
);
}
// Import and use ServiceManager's configureServices
const { ServiceManager } =
await import("../mod_services/classes.servicemanager.js");
const serviceManager = new ServiceManager();
await serviceManager.init();
await serviceManager.configureServices();
}
async function handleGet(
configPath: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
const smartconfigData = await readSmartconfigFile();
const value = getCliConfigValueFromData(smartconfigData, configPath);
if (mode.json) {
printJson({ path: configPath, value, exists: value !== undefined });
return;
}
if (value === undefined) {
plugins.logger.log("warn", `No value set for ${configPath}`);
return;
}
if (typeof value === "string") {
console.log(value);
return;
}
printJson(value);
}
async function handleSet(
configPath: string | undefined,
rawValue: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
if (rawValue === undefined) {
throw new Error("Configuration value is required");
}
const smartconfigData = await readSmartconfigFile();
const parsedValue = parseConfigValue(rawValue);
setCliConfigValueInData(smartconfigData, configPath, parsedValue);
await writeSmartconfigFile(smartconfigData);
if (mode.json) {
printJson({
ok: true,
action: "set",
path: configPath,
value: parsedValue,
});
return;
}
plugins.logger.log("success", `Set ${configPath}`);
}
async function handleUnset(
configPath: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
const smartconfigData = await readSmartconfigFile();
const removed = unsetCliConfigValueInData(smartconfigData, configPath);
if (!removed) {
if (mode.json) {
printJson({
ok: false,
action: "unset",
path: configPath,
removed: false,
});
return;
}
plugins.logger.log("warn", `No value set for ${configPath}`);
return;
}
await writeSmartconfigFile(smartconfigData);
if (mode.json) {
printJson({ ok: true, action: "unset", path: configPath, removed: true });
return;
}
plugins.logger.log("success", `Unset ${configPath}`);
}
async function handleMigrate(
rawTargetVersion: string | undefined,
mode: ICliMode,
): Promise<void> {
const targetVersion = rawTargetVersion
? Number(rawTargetVersion)
: CURRENT_GITZONE_CLI_SCHEMA_VERSION;
if (!Number.isInteger(targetVersion) || targetVersion < 1) {
throw new Error("Migration target version must be a positive integer");
}
const smartconfigData = await readSmartconfigFile();
const result = migrateSmartconfigData(smartconfigData, targetVersion);
if (result.migrated) {
await writeSmartconfigFile(smartconfigData);
}
if (mode.json) {
printJson({ ok: true, action: "migrate", ...result });
return;
}
if (result.migrated) {
plugins.logger.log(
"success",
`Migrated .smartconfig.json from schema v${result.fromVersion} to v${result.toVersion}`,
);
} else {
plugins.logger.log("info", `.smartconfig.json already at schema v${result.toVersion}`);
}
}
function parseConfigValue(rawValue: string): any {
const trimmedValue = rawValue.trim();
if (trimmedValue === "true") {
return true;
}
if (trimmedValue === "false") {
return false;
}
if (trimmedValue === "null") {
return null;
}
if (/^-?\d+(\.\d+)?$/.test(trimmedValue)) {
return Number(trimmedValue);
}
if (
(trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) ||
(trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) ||
(trimmedValue.startsWith('"') && trimmedValue.endsWith('"'))
) {
return JSON.parse(trimmedValue);
}
return rawValue;
}
type TDoctorFindingLevel = "ok" | "warn" | "error";
interface IDoctorFinding {
level: TDoctorFindingLevel;
message: string;
fix?: string;
}
const validProjectTypes = ["npm", "service", "wcc", "website"];
const validConfirmationModes = ["prompt", "auto", "plan"];
const validCommitSteps = [
"format",
"analyze",
"test",
"build",
"changelog",
"commit",
"push",
];
function printConfigSection(
title: string,
rows: Array<[string, string]>,
): void {
console.log(`${title}:`);
for (const [label, value] of rows) {
console.log(` ${label.padEnd(20)} ${value}`);
}
console.log("");
}
function formatValue(value: unknown): string {
if (value === undefined || value === null || value === "") {
return "(unset)";
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "string" || typeof value === "number") {
return String(value);
}
return JSON.stringify(value);
}
function formatRepository(moduleConfig: any): string {
if (!moduleConfig) {
return "(unset)";
}
const parts = [
moduleConfig.githost,
moduleConfig.gitscope,
moduleConfig.gitrepo,
].filter(Boolean);
return parts.length > 0 ? parts.join("/") : "(unset)";
}
function formatList(value: unknown): string {
if (!Array.isArray(value) || value.length === 0) {
return "(none)";
}
return value.map((item) => String(item)).join(", ");
}
function formatTarget(enabled: unknown, targetConfig: any): string {
const state = enabled === false ? "disabled" : "enabled";
if (!targetConfig || Object.keys(targetConfig).length === 0) {
return state;
}
const details: string[] = [];
if (targetConfig.remote) details.push(`remote=${targetConfig.remote}`);
if (Array.isArray(targetConfig.registries)) {
details.push(`registries=${targetConfig.registries.length}`);
}
if (targetConfig.accessLevel) details.push(`access=${targetConfig.accessLevel}`);
if (Array.isArray(targetConfig.images)) {
details.push(`images=${targetConfig.images.length}`);
}
return details.length > 0 ? `${state} (${details.join(", ")})` : state;
}
async function askValue<T>(
interactInstance: any,
options: any,
): Promise<T> {
const response = await interactInstance.askQuestion(options);
return response.value as T;
}
async function readPackageJson(): Promise<Record<string, any>> {
const packageJsonPath = plugins.path.join(process.cwd(), "package.json");
if (!(await plugins.smartfs.file(packageJsonPath).exists())) {
return {};
}
const content = (await plugins.smartfs
.file(packageJsonPath)
.encoding("utf8")
.read()) as string;
return JSON.parse(content);
}
function inferRepoName(packageName: unknown): string {
if (typeof packageName === "string" && packageName.trim()) {
const normalizedName = packageName.trim();
return normalizedName.includes("/")
? normalizedName.split("/").pop() || normalizedName
: normalizedName;
}
return plugins.path.basename(process.cwd());
}
function parseCsv(value: string): string[] {
const result: string[] = [];
for (const item of value.split(",")) {
const trimmedItem = item.trim();
if (trimmedItem && !result.includes(trimmedItem)) {
result.push(trimmedItem);
}
}
return result;
}
function normalizeRegistryUrl(url: string): string {
let normalizedUrl = url.trim();
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
normalizedUrl = `https://${normalizedUrl}`;
}
return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
}
function getDefaultEnabledTargets(currentTargets: Record<string, any>): string[] {
const enabledTargets: string[] = [];
const npmRegistries = currentTargets.npm?.registries;
if (currentTargets.git?.enabled ?? true) {
enabledTargets.push("git");
}
if (currentTargets.npm?.enabled ?? (Array.isArray(npmRegistries) && npmRegistries.length > 0)) {
enabledTargets.push("npm");
}
if (currentTargets.docker?.enabled ?? false) {
enabledTargets.push("docker");
}
return enabledTargets;
}
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({
ok: counts.error === 0,
counts,
findings,
});
} else {
console.log("");
console.log("gitzone config doctor");
console.log("");
for (const finding of findings) {
const prefix = finding.level.toUpperCase().padEnd(5);
console.log(`${prefix} ${finding.message}`);
if (finding.fix) {
console.log(` ${finding.fix}`);
}
}
console.log("");
console.log(`Summary: ${counts.ok} ok, ${counts.warn} warning, ${counts.error} error`);
console.log("");
}
if (counts.error > 0) {
process.exitCode = 1;
}
}
function validateCommitConfig(
commitConfig: Record<string, any>,
findings: IDoctorFinding[],
): void {
const confirmation = commitConfig.confirmation;
if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
findings.push({ level: "ok", message: "Commit confirmation mode is valid" });
} else {
findings.push({
level: "warn",
message: `Invalid commit confirmation mode: ${formatValue(confirmation)}`,
fix: "Use prompt, auto, or plan.",
});
}
const steps = commitConfig.steps;
if (steps === undefined) {
findings.push({ level: "ok", message: "Commit workflow uses default steps" });
return;
}
if (!Array.isArray(steps) || steps.length === 0) {
findings.push({
level: "error",
message: "Commit steps must be a non-empty array",
fix: "Run `gitzone config commit` or unset commit.steps.",
});
return;
}
const invalidSteps = steps.filter((step) => !validCommitSteps.includes(step));
if (invalidSteps.length > 0) {
findings.push({
level: "error",
message: `Invalid commit steps: ${invalidSteps.join(", ")}`,
fix: `Allowed steps: ${validCommitSteps.join(", ")}`,
});
}
const analyzeIndex = steps.indexOf("analyze");
const changelogIndex = steps.indexOf("changelog");
const commitIndex = steps.indexOf("commit");
if (analyzeIndex === -1 || changelogIndex === -1 || commitIndex === -1) {
findings.push({
level: "error",
message: "Commit workflow must include analyze, changelog, and commit",
fix: "Run `gitzone config commit` or reset commit.steps.",
});
} else if (analyzeIndex > commitIndex || changelogIndex > commitIndex) {
findings.push({
level: "error",
message: "Commit workflow must run analyze and changelog before commit",
fix: "Move analyze and changelog before commit in commit.steps.",
});
} else {
findings.push({ level: "ok", message: "Commit workflow steps are valid" });
}
if (steps.includes("test") && commitConfig.test?.command === "") {
findings.push({
level: "warn",
message: "Commit test step has an empty command",
fix: "Set commit.test.command or unset it to use the default pnpm test.",
});
}
if (steps.includes("build") && commitConfig.build?.command === "") {
findings.push({
level: "warn",
message: "Commit build step has an empty command",
fix: "Set commit.build.command or unset it to use the default pnpm build.",
});
}
}
async function validateReleaseConfig(
releaseConfig: Record<string, any>,
findings: IDoctorFinding[],
): Promise<void> {
const confirmation = releaseConfig.confirmation;
if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
findings.push({ level: "ok", message: "Release confirmation mode is valid" });
} else {
findings.push({
level: "warn",
message: `Invalid release confirmation mode: ${formatValue(confirmation)}`,
fix: "Use prompt, auto, or plan.",
});
}
if (releaseConfig.registries || releaseConfig.accessLevel || releaseConfig.steps || releaseConfig.changelog) {
findings.push({
level: "warn",
message: "Legacy release keys are present outside release.targets",
fix: "Run `gitzone config migrate`.",
});
}
const preflight = releaseConfig.preflight || {};
if (preflight.test === true && preflight.testCommand === "") {
findings.push({
level: "warn",
message: "Release test preflight has an empty command",
fix: "Set release.preflight.testCommand or unset it to use pnpm test.",
});
}
if (preflight.build === true && preflight.buildCommand === "") {
findings.push({
level: "warn",
message: "Release build preflight has an empty command",
fix: "Set release.preflight.buildCommand or unset it to use pnpm build.",
});
}
const targets = releaseConfig.targets || {};
await validateGitTarget(targets.git || {}, findings);
await validateNpmTarget(targets.npm || {}, findings);
validateDockerTarget(targets.docker || {}, findings);
}
async function validateGitTarget(
gitTarget: Record<string, any>,
findings: IDoctorFinding[],
): Promise<void> {
const enabled = gitTarget.enabled ?? true;
if (!enabled) {
findings.push({ level: "ok", message: "Git release target is disabled" });
return;
}
if (gitTarget.remote === "") {
findings.push({
level: "error",
message: "Git release target remote is empty",
fix: "Set release.targets.git.remote or unset it to use origin.",
});
return;
}
findings.push({
level: "ok",
message: `Git release target is enabled (${gitTarget.remote || "origin"})`,
});
}
async function validateNpmTarget(
npmTarget: Record<string, any>,
findings: IDoctorFinding[],
): Promise<void> {
const registries = Array.isArray(npmTarget.registries) ? npmTarget.registries : [];
const enabled = npmTarget.enabled ?? registries.length > 0;
if (!enabled) {
findings.push({ level: "ok", message: "npm release target is disabled" });
return;
}
if (registries.length === 0) {
findings.push({
level: "error",
message: "npm release target is enabled without registries",
fix: "Run `gitzone config add https://registry.npmjs.org` or disable release.targets.npm.enabled.",
});
return;
}
if (npmTarget.accessLevel && !["public", "private"].includes(npmTarget.accessLevel)) {
findings.push({
level: "error",
message: `Invalid npm access level: ${npmTarget.accessLevel}`,
fix: "Use public or private.",
});
}
if (npmTarget.alreadyPublished && !["success", "error"].includes(npmTarget.alreadyPublished)) {
findings.push({
level: "error",
message: `Invalid npm alreadyPublished behavior: ${npmTarget.alreadyPublished}`,
fix: "Use success or error.",
});
}
const smartNetwork = new plugins.smartnetwork.SmartNetwork();
await Promise.all(
registries.map(async (registry) => {
await validateNpmRegistry(registry, smartNetwork, findings);
await validateNpmAuth(registry, findings);
}),
);
}
async function validateNpmRegistry(
registry: string,
smartNetwork: plugins.smartnetwork.SmartNetwork,
findings: IDoctorFinding[],
): Promise<void> {
const normalizedRegistry = normalizeRegistryUrl(registry);
if (normalizedRegistry !== registry) {
findings.push({
level: "warn",
message: `npm registry should be normalized: ${registry}`,
fix: `Use ${normalizedRegistry}`,
});
}
let registryUrl: URL;
try {
registryUrl = new URL(normalizedRegistry);
} catch {
findings.push({
level: "error",
message: `Invalid npm registry URL: ${registry}`,
fix: "Use a valid http or https registry URL.",
});
return;
}
if (registryUrl.protocol !== "https:" && registryUrl.protocol !== "http:") {
findings.push({
level: "error",
message: `Unsupported npm registry protocol: ${registryUrl.protocol}`,
fix: "Use an http or https registry URL.",
});
return;
}
try {
const result = await smartNetwork.checkEndpoint(normalizedRegistry, { timeout: 5000 });
if (result.status >= 200 && result.status < 500) {
findings.push({
level: "ok",
message: `npm registry is reachable: ${normalizedRegistry} (${result.status})`,
});
} else {
findings.push({
level: "warn",
message: `npm registry returned status ${result.status}: ${normalizedRegistry}`,
});
}
} catch (error) {
findings.push({
level: "warn",
message: `npm registry is not reachable: ${normalizedRegistry}`,
fix: error instanceof Error ? error.message : String(error),
});
}
}
async function validateNpmAuth(
registry: string,
findings: IDoctorFinding[],
): Promise<void> {
const normalizedRegistry = normalizeRegistryUrl(registry);
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: "bash",
sourceFilePaths: [],
});
try {
const result = await smartshellInstance.execSpawn(
"pnpm",
["npm", "whoami", `--registry=${normalizedRegistry}`],
{ silent: true, timeout: 8000 },
);
if (result.exitCode === 0) {
findings.push({
level: "ok",
message: `npm auth is available for ${normalizedRegistry}`,
});
} else {
findings.push({
level: "warn",
message: `npm auth is missing or invalid for ${normalizedRegistry}`,
fix: `Run pnpm npm login --registry=${normalizedRegistry}`,
});
}
} catch (error) {
findings.push({
level: "warn",
message: `Could not check npm auth for ${normalizedRegistry}`,
fix: error instanceof Error ? error.message : String(error),
});
}
}
function validateDockerTarget(
dockerTarget: Record<string, any>,
findings: IDoctorFinding[],
): void {
const enabled = dockerTarget.enabled ?? false;
if (!enabled) {
findings.push({ level: "ok", message: "Docker release target is disabled" });
return;
}
if (!Array.isArray(dockerTarget.images) || dockerTarget.images.length === 0) {
findings.push({
level: "error",
message: "Docker release target is enabled without images",
fix: "Set release.targets.docker.images or disable release.targets.docker.enabled.",
});
return;
}
findings.push({
level: "ok",
message: `Docker release target has ${dockerTarget.images.length} image template(s)`,
});
}
async function validateDetectedProjectType(
cliConfig: Record<string, any>,
findings: IDoctorFinding[],
): Promise<void> {
const packageJsonPath = plugins.path.join(process.cwd(), "package.json");
const denoJsonPath = plugins.path.join(process.cwd(), "deno.json");
const hasPackageJson = await plugins.smartfs.file(packageJsonPath).exists();
const hasDenoJson = await plugins.smartfs.file(denoJsonPath).exists();
if (!hasPackageJson && !hasDenoJson) {
findings.push({
level: "warn",
message: "Could not detect package.json or deno.json",
fix: "Run this command from a project root.",
});
return;
}
if (hasPackageJson && validProjectTypes.includes(cliConfig.projectType)) {
findings.push({
level: "ok",
message: "Detected project files match configured npm-compatible project type",
});
return;
}
if (hasDenoJson && !hasPackageJson) {
findings.push({
level: "warn",
message: "Detected a Deno-only project, but guided config supports npm-compatible project types",
fix: "Use `gitzone config set projectType npm|service|wcc|website` only for npm-compatible projects.",
});
}
}
/**
* Show help for config command
*/
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "config",
usage: "gitzone config <command> [options]",
commands: [
{
name: "show",
description: "Display current @git.zone/cli configuration",
},
{ name: "project", description: "Configure project basics interactively" },
{ 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" },
{ name: "add [url]", description: "Add an npm release target registry" },
{ name: "remove [url]", description: "Remove an npm release target registry" },
{ name: "clear", description: "Clear npm release target registries" },
{
name: "access [public|private]",
description: "Set npm target publish access level",
},
{
name: "commit <setting> <value>",
description: "Set commit defaults",
},
{
name: "migrate [version]",
description: "Run version-targeted .smartconfig.json migrations",
},
],
examples: [
"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",
],
});
return;
}
console.log("");
console.log("Usage: gitzone config <command> [options]");
console.log("");
console.log("Commands:");
console.log(
" show Display current @git.zone/cli configuration",
);
console.log(" project Configure project basics interactively");
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");
console.log(" add [url] Add an npm target registry URL");
console.log(" remove [url] Remove an npm target registry URL");
console.log(" clear Clear npm target registries");
console.log(
" access [public|private] Set npm target access level for publishing",
);
console.log(" commit [setting] [value] Configure commit options");
console.log(" migrate [version] Run version-targeted smartconfig migrations");
console.log(
" services Configure which services are enabled",
);
console.log("");
console.log("Examples:");
console.log(" gitzone config show");
console.log(" gitzone config show --json");
console.log(" gitzone config project");
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");
console.log(" gitzone config unset cli.output");
console.log(" gitzone config add https://registry.npmjs.org");
console.log(" gitzone config add https://verdaccio.example.com");
console.log(" gitzone config remove https://registry.npmjs.org");
console.log(" gitzone config clear");
console.log(" gitzone config access public");
console.log(" gitzone config access private");
console.log(" gitzone config migrate 2");
console.log(" gitzone config commit # Interactive");
console.log(" gitzone config commit alwaysTest true");
console.log(" gitzone config services # Interactive");
console.log("");
}