// gitzone config - manage release registry 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 { getCliConfigValueFromData, readSmartconfigFile, setCliConfigValueInData, unsetCliConfigValueInData, writeSmartconfigFile, } from "../helpers.smartconfig.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 { 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 "services": await handleServices(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 { 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: "Add a registry", value: "add" }, { name: "Remove a registry", value: "remove" }, { name: "Clear all registries", value: "clear" }, { name: "Set access level (public/private)", value: "access" }, { name: "Configure commit options", value: "commit" }, { name: "Configure services", value: "services" }, { name: "Show help", value: "help" }, ], }); const action = (response as any).value; switch (action) { case "show": await handleShow(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 "commit": await handleCommit(undefined, undefined, defaultCliMode); break; case "services": await handleServices(defaultCliMode); break; case "help": showHelp(); break; } } /** * Show current registry configuration */ async function handleShow(mode: ICliMode): Promise { if (mode.json) { const smartconfigData = await readSmartconfigFile(); printJson(getCliConfigValueFromData(smartconfigData, "")); return; } const config = await ReleaseConfig.fromCwd(); const registries = config.getRegistries(); const accessLevel = config.getAccessLevel(); console.log(""); console.log( "╭─────────────────────────────────────────────────────────────╮", ); console.log( "│ Release Configuration │", ); console.log( "╰─────────────────────────────────────────────────────────────╯", ); console.log(""); // Show access level plugins.logger.log("info", `Access Level: ${accessLevel}`); console.log(""); if (registries.length === 0) { plugins.logger.log("info", "No release registries configured."); console.log(""); console.log(" Run `gitzone config add ` to add one."); console.log(""); } else { plugins.logger.log("info", `Configured registries (${registries.length}):`); console.log(""); registries.forEach((url, index) => { console.log(` ${index + 1}. ${url}`); }); console.log(""); } } /** * Add a registry URL */ async function handleAdd( url: string | undefined, mode: ICliMode, ): Promise { 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 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 registry: ${url}`); await formatSmartconfigWithDiff(mode); } else { plugins.logger.log("warn", `Registry already exists: ${url}`); } } /** * Remove a registry URL */ async function handleRemove( url: string | undefined, mode: ICliMode, ): Promise { const config = await ReleaseConfig.fromCwd(); const registries = config.getRegistries(); if (registries.length === 0) { plugins.logger.log("warn", "No 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 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 registry: ${url}`); await formatSmartconfigWithDiff(mode); } else { plugins.logger.log("warn", `Registry not found: ${url}`); } } /** * Clear all registries */ async function handleClear(mode: ICliMode): Promise { const config = await ReleaseConfig.fromCwd(); if (!config.hasRegistries()) { plugins.logger.log("info", "No registries to clear."); return; } // Confirm before clearing const confirmed = mode.interactive ? await plugins.smartinteract.SmartInteract.getCliConfirmation( "Clear all configured 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 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 { 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 { 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 { 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")); 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 { // Parse boolean value const boolValue = value === "true" || value === "1" || value === "on"; if (setting === "alwaysTest") { config.setAlwaysTest(boolValue); } else if (setting === "alwaysBuild") { config.setAlwaysBuild(boolValue); } 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); } /** * 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(""); } /** * Handle services configuration */ async function handleServices(mode: ICliMode): Promise { 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 { 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 { 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 { 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}`); } 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; } /** * Show help for config command */ export function showHelp(mode?: ICliMode): void { if (mode?.json) { printJson({ command: "config", usage: "gitzone config [options]", commands: [ { name: "show", description: "Display current @git.zone/cli configuration", }, { name: "get ", description: "Read a single config value" }, { name: "set ", description: "Write a config value" }, { name: "unset ", description: "Delete a config value" }, { name: "add [url]", description: "Add a release registry" }, { name: "remove [url]", description: "Remove a release registry" }, { name: "clear", description: "Clear all release registries" }, { name: "access [public|private]", description: "Set npm publish access level", }, { name: "commit ", description: "Set commit defaults", }, ], examples: [ "gitzone config show --json", "gitzone config get release.accessLevel", "gitzone config set cli.interactive false", "gitzone config set cli.output json", ], }); return; } console.log(""); console.log("Usage: gitzone config [options]"); console.log(""); console.log("Commands:"); console.log( " show Display current @git.zone/cli configuration", ); console.log(" get Read a single config value"); console.log(" set Write a config value"); console.log(" unset Delete a config value"); console.log(" add [url] Add a registry URL"); console.log(" remove [url] Remove a registry URL"); console.log(" clear Clear all registries"); console.log( " access [public|private] Set npm access level for publishing", ); console.log(" commit [setting] [value] Configure commit options"); 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 get release.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 commit # Interactive"); console.log(" gitzone config commit alwaysTest true"); console.log(" gitzone config services # Interactive"); console.log(""); }