import { getCliConfigValue } from "./helpers.smartconfig.js"; export type TConfirmationMode = "prompt" | "auto" | "plan"; export type TCommitStep = | "format" | "analyze" | "test" | "build" | "changelog" | "commit" | "push"; export type TReleaseTarget = "git" | "npm" | "docker"; export interface ICommitWorkflowConfig { confirmation?: TConfirmationMode; staging?: "all"; steps?: TCommitStep[]; alwaysTest?: boolean; alwaysBuild?: boolean; analyze?: { provider?: "ai"; requireConfirmationFor?: string[]; }; test?: { command?: string; }; build?: { command?: string; verifyCleanTree?: boolean; }; push?: { remote?: string; followTags?: boolean; }; } export interface IReleaseGitTargetConfig { enabled?: boolean; remote?: string; pushBranch?: boolean; pushTags?: boolean; } export interface IReleaseNpmTargetConfig { enabled?: boolean; registries?: string[]; accessLevel?: "public" | "private"; alreadyPublished?: "success" | "error"; } export interface IReleaseDockerTargetConfig { enabled?: boolean; images?: string[]; } export interface IReleaseWorkflowConfig { confirmation?: TConfirmationMode; version?: { strategy?: "semver"; source?: "pendingChangelog" | "manual"; }; preflight?: { requireCleanTree?: boolean; test?: boolean; build?: boolean; testCommand?: string; buildCommand?: string; }; targets?: { git?: IReleaseGitTargetConfig; npm?: IReleaseNpmTargetConfig; docker?: IReleaseDockerTargetConfig; }; } export interface IResolvedCommitWorkflow { confirmation: TConfirmationMode; steps: TCommitStep[]; staging: "all"; testCommand: string; buildCommand: string; changelogFile: "changelog.md"; changelogSection: "Pending"; pushRemote: string; pushFollowTags: boolean; releaseFlagRequested: boolean; } export interface IResolvedReleaseWorkflow { confirmation: TConfirmationMode; plan: string[]; targets: TReleaseTarget[]; requireCleanTree: boolean; runTests: boolean; runBuild: boolean; testCommand: string; buildCommand: string; changelogFile: "changelog.md"; changelogPendingSection: "Pending"; changelogVersionHeading: "## {{date}} - {{version}}"; gitEnabled: boolean; gitRemote: string; pushBranch: boolean; pushTags: boolean; npmEnabled: boolean; npmRegistries: string[]; npmAccessLevel: "public" | "private"; npmAlreadyPublished: "success" | "error"; dockerEnabled: boolean; dockerImages: string[]; } interface ICliWorkflowConfig { commit?: ICommitWorkflowConfig; release?: IReleaseWorkflowConfig; } const commitFlagToStep: Record = { f: "format", t: "test", b: "build", p: "push", }; const unique = (items: T[]): T[] => { const result: T[] = []; for (const item of items) { if (!result.includes(item)) { result.push(item); } } return result; }; const normalizeConfirmation = ( value: unknown, fallback: TConfirmationMode, ): TConfirmationMode => { if (value === "prompt" || value === "auto" || value === "plan") { return value; } return fallback; }; const 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; }; const isDisabled = (argvArg: any, ...keys: string[]): boolean => { return keys.some((key) => argvArg[key] === false || argvArg[`no-${key}`] || argvArg[`no${key[0].toUpperCase()}${key.slice(1)}`]); }; const readCliWorkflowConfig = async (): Promise => { return await getCliConfigValue("", {}); }; const getOrderedArgsAfterCommand = (commandName: string): string[] => { const rawArgs = process.argv.slice(2); const commandIndex = rawArgs.indexOf(commandName); if (commandIndex === -1) { return rawArgs; } return rawArgs.slice(commandIndex + 1); }; const getOrderedShortFlags = (commandName: string): string[] => { const orderedFlags: string[] = []; for (const arg of getOrderedArgsAfterCommand(commandName)) { if (arg === "--") { break; } if (arg.startsWith("--")) { continue; } if (arg.startsWith("-") && arg.length > 1) { orderedFlags.push(...arg.slice(1).split("")); } } return orderedFlags; }; const hasExplicitCommitWorkflowFlags = (argvArg: any): boolean => { return Boolean( argvArg.f || argvArg.format || argvArg.t || argvArg.test || argvArg.b || argvArg.build || argvArg.p || argvArg.push, ); }; const normalizeCommitSteps = (rawSteps: TCommitStep[]): TCommitStep[] => { const steps = unique(rawSteps.filter(Boolean)); const pushRequested = steps.includes("push"); const prePushSteps = steps.filter((step) => step !== "push"); if (!prePushSteps.includes("analyze")) { prePushSteps.unshift("analyze"); } if (!prePushSteps.includes("changelog")) { const commitIndex = prePushSteps.indexOf("commit"); if (commitIndex === -1) { prePushSteps.push("changelog"); } else { prePushSteps.splice(commitIndex, 0, "changelog"); } } if (!prePushSteps.includes("commit")) { prePushSteps.push("commit"); } const analyzeIndex = prePushSteps.indexOf("analyze"); const commitIndex = prePushSteps.indexOf("commit"); if (analyzeIndex > commitIndex) { throw new Error("Commit workflow requires analyze before commit."); } const changelogIndex = prePushSteps.indexOf("changelog"); if (changelogIndex === -1 || changelogIndex > commitIndex) { throw new Error("Commit workflow requires changelog before commit."); } return pushRequested ? [...prePushSteps, "push"] : prePushSteps; }; const getTargetOverride = (argvArg: any): TReleaseTarget[] | undefined => { const validTargets: TReleaseTarget[] = ["git", "npm", "docker"]; const rawTargets = argvArg.target || argvArg.targets; if (typeof rawTargets === "string") { return rawTargets .split(",") .map((target) => target.trim()) .filter((target): target is TReleaseTarget => validTargets.includes(target as TReleaseTarget)); } const targets: TReleaseTarget[] = []; if (argvArg.git || argvArg.p || argvArg.push) targets.push("git"); if (argvArg.npm) targets.push("npm"); if (argvArg.docker) targets.push("docker"); return targets.length > 0 ? targets : undefined; }; const buildReleasePlan = (options: { requireCleanTree: boolean; runTests: boolean; runBuild: boolean; targets: TReleaseTarget[]; }): string[] => { const plan: string[] = []; if (options.requireCleanTree) plan.push("preflight.cleanTree"); if (options.runTests) plan.push("preflight.test"); plan.push("core.version", "core.changelog", "core.commit", "core.tag"); if (options.runBuild) plan.push("core.build"); for (const target of options.targets) { plan.push(`target.${target}`); } return plan; }; export const resolveCommitWorkflow = async (argvArg: any): Promise => { const cliConfig = await readCliWorkflowConfig(); const commitConfig = cliConfig.commit || {}; const releaseFlagRequested = Boolean(argvArg.r || argvArg.release); let confirmation = normalizeConfirmation(commitConfig.confirmation, "prompt"); if (argvArg.plan) { confirmation = "plan"; } else if (argvArg.y || argvArg.yes) { confirmation = "auto"; } let rawSteps: TCommitStep[]; if (hasExplicitCommitWorkflowFlags(argvArg)) { const orderedFlags = getOrderedShortFlags("commit"); rawSteps = ["analyze"]; for (const shortFlag of orderedFlags) { const step = commitFlagToStep[shortFlag]; if (step) { rawSteps.push(step); } } if (argvArg.format && !rawSteps.includes("format")) rawSteps.push("format"); if (argvArg.test && !rawSteps.includes("test")) rawSteps.push("test"); if (argvArg.build && !rawSteps.includes("build")) rawSteps.push("build"); if (argvArg.push && !rawSteps.includes("push")) rawSteps.push("push"); rawSteps.push("changelog"); rawSteps.push("commit"); } else if (Array.isArray(commitConfig.steps) && commitConfig.steps.length > 0) { rawSteps = commitConfig.steps; } else { rawSteps = ["analyze"]; if (commitConfig.alwaysTest) rawSteps.push("test"); if (commitConfig.alwaysBuild) rawSteps.push("build"); rawSteps.push("changelog"); rawSteps.push("commit"); } return { confirmation, steps: normalizeCommitSteps(rawSteps), staging: commitConfig.staging || "all", testCommand: commitConfig.test?.command || "pnpm test", buildCommand: commitConfig.build?.command || "pnpm build", changelogFile: "changelog.md", changelogSection: "Pending", pushRemote: commitConfig.push?.remote || "origin", pushFollowTags: commitConfig.push?.followTags || false, releaseFlagRequested, }; }; export const resolveReleaseWorkflow = async (argvArg: any): Promise => { const cliConfig = await readCliWorkflowConfig(); const releaseConfig = cliConfig.release || {}; const targetConfig = releaseConfig.targets || {}; const gitConfig = targetConfig.git || {}; const npmConfig = targetConfig.npm || {}; const dockerConfig = targetConfig.docker || {}; const npmRegistries = (npmConfig.registries || []).map(normalizeRegistryUrl); const npmEnabled = npmConfig.enabled ?? npmRegistries.length > 0; const gitEnabled = gitConfig.enabled ?? true; const dockerEnabled = dockerConfig.enabled ?? false; let confirmation = normalizeConfirmation(releaseConfig.confirmation, "prompt"); if (argvArg.plan) { confirmation = "plan"; } else if (argvArg.y || argvArg.yes) { confirmation = "auto"; } let requireCleanTree = releaseConfig.preflight?.requireCleanTree ?? true; let runTests = releaseConfig.preflight?.test ?? false; let runBuild = releaseConfig.preflight?.build ?? true; if (argvArg.t || argvArg.test) runTests = true; if (argvArg.b || argvArg.build) runBuild = true; if (isDisabled(argvArg, "test")) runTests = false; if (isDisabled(argvArg, "build")) runBuild = false; if (isDisabled(argvArg, "preflight")) requireCleanTree = false; const configuredTargets: TReleaseTarget[] = []; if (gitEnabled) configuredTargets.push("git"); if (npmEnabled) configuredTargets.push("npm"); if (dockerEnabled) configuredTargets.push("docker"); let targets = getTargetOverride(argvArg) || configuredTargets; if (isDisabled(argvArg, "git", "push")) { targets = targets.filter((target) => target !== "git"); } if (isDisabled(argvArg, "publish")) { targets = targets.filter((target) => target === "git"); } targets = unique(targets); return { confirmation, plan: buildReleasePlan({ requireCleanTree, runTests, runBuild, targets }), targets, requireCleanTree, runTests, runBuild, testCommand: releaseConfig.preflight?.testCommand || "pnpm test", buildCommand: releaseConfig.preflight?.buildCommand || "pnpm build", changelogFile: "changelog.md", changelogPendingSection: "Pending", changelogVersionHeading: "## {{date}} - {{version}}", gitEnabled, gitRemote: gitConfig.remote || "origin", pushBranch: gitConfig.pushBranch ?? true, pushTags: gitConfig.pushTags ?? true, npmEnabled, npmRegistries, npmAccessLevel: npmConfig.accessLevel || "public", npmAlreadyPublished: npmConfig.alreadyPublished || "success", dockerEnabled, dockerImages: dockerConfig.images || [], }; };