Files
cli/ts/mod_config/index.ts
T

740 lines
21 KiB
TypeScript

// 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<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 "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<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: "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<void> {
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 <registry-url>` 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<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 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<void> {
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<void> {
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<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"));
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);
}
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<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}`);
}
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 <command> [options]",
commands: [
{
name: "show",
description: "Display current @git.zone/cli configuration",
},
{ 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 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 <setting> <value>",
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 <command> [options]");
console.log("");
console.log("Commands:");
console.log(
" show Display current @git.zone/cli configuration",
);
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 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("");
}