From 48c4b0c9b2983648dda0650aa11b4973a1421c7d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 14 Dec 2025 01:31:06 +0000 Subject: [PATCH] update --- ts/gitzone.cli.ts | 8 ++ ts/mod_commit/index.ts | 78 +++++++++++- ts/mod_commit/mod.ui.ts | 13 +- ts/mod_config/classes.releaseconfig.ts | 144 +++++++++++++++++++++ ts/mod_config/index.ts | 169 +++++++++++++++++++++++++ ts/mod_config/mod.plugins.ts | 3 + 6 files changed, 407 insertions(+), 8 deletions(-) create mode 100644 ts/mod_config/classes.releaseconfig.ts create mode 100644 ts/mod_config/index.ts create mode 100644 ts/mod_config/mod.plugins.ts diff --git a/ts/gitzone.cli.ts b/ts/gitzone.cli.ts index ad9d1a3..28ca5b9 100644 --- a/ts/gitzone.cli.ts +++ b/ts/gitzone.cli.ts @@ -131,6 +131,14 @@ export let run = async () => { modHelpers.run(argvArg); }); + /** + * manage release configuration + */ + gitzoneSmartcli.addCommand('config').subscribe(async (argvArg) => { + const modConfig = await import('./mod_config/index.js'); + await modConfig.run(argvArg); + }); + /** * manage development services (MongoDB, S3/MinIO) */ diff --git a/ts/mod_commit/index.ts b/ts/mod_commit/index.ts index b74c069..1579b39 100644 --- a/ts/mod_commit/index.ts +++ b/ts/mod_commit/index.ts @@ -5,8 +5,24 @@ import * as paths from '../paths.js'; import { logger } from '../gitzone.logging.js'; import * as helpers from './mod.helpers.js'; import * as ui from './mod.ui.js'; +import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js'; export const run = async (argvArg: any) => { + // Check if release flag is set and validate registries early + const wantsRelease = !!(argvArg.r || argvArg.release); + let releaseConfig: ReleaseConfig | null = null; + + if (wantsRelease) { + releaseConfig = await ReleaseConfig.fromCwd(); + if (!releaseConfig.hasRegistries()) { + logger.log('error', 'No release registries configured.'); + console.log(''); + console.log(' Run `gitzone config add ` to add registries.'); + console.log(''); + process.exit(1); + } + } + if (argvArg.format) { const formatMod = await import('../mod_format/index.js'); await formatMod.run(); @@ -56,6 +72,10 @@ export const run = async (argvArg: any) => { name: 'pushToOrigin', value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided }); + answerBucket.addAnswer({ + name: 'createRelease', + value: wantsRelease, + }); } else { // Warn if --yes was provided but we're requiring confirmation due to breaking change if (isBreakingChange && (argvArg.y || argvArg.yes)) { @@ -89,6 +109,12 @@ export const run = async (argvArg: any) => { message: `Do you want to push this version now?`, default: true, }, + { + type: 'confirm', + name: `createRelease`, + message: `Do you want to publish to npm registries?`, + default: wantsRelease, + }, ]); answerBucket = await commitInteract.runQueue(); } @@ -111,8 +137,24 @@ export const run = async (argvArg: any) => { sourceFilePaths: [], }); - // Determine total steps (6 if pushing, 5 if not) - const totalSteps = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true') ? 6 : 5; + // Load release config if user wants to release (interactively selected) + if (answerBucket.getAnswerFor('createRelease') && !releaseConfig) { + releaseConfig = await ReleaseConfig.fromCwd(); + if (!releaseConfig.hasRegistries()) { + logger.log('error', 'No release registries configured.'); + console.log(''); + console.log(' Run `gitzone config add ` to add registries.'); + console.log(''); + process.exit(1); + } + } + + // Determine total steps based on options + const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'); + const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries(); + let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version + if (willPush) totalSteps++; + if (willRelease) totalSteps++; let currentStep = 0; // Step 1: Baking commitinfo @@ -175,16 +217,36 @@ export const run = async (argvArg: any) => { // Step 6: Push to remote (optional) const currentBranch = await helpers.detectCurrentBranch(); - if ( - answerBucket.getAnswerFor('pushToOrigin') && - !(process.env.CI === 'true') - ) { + if (willPush) { currentStep++; ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress'); await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`); ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'done'); } + // Step 7: Publish to npm registries (optional) + let releasedRegistries: string[] = []; + if (willRelease && releaseConfig) { + currentStep++; + const registries = releaseConfig.getRegistries(); + ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'in-progress'); + + for (const registry of registries) { + try { + await smartshellInstance.exec(`npm publish --registry=${registry}`); + releasedRegistries.push(registry); + } catch (error) { + logger.log('error', `Failed to publish to ${registry}: ${error}`); + } + } + + if (releasedRegistries.length === registries.length) { + ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'done'); + } else { + ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'error'); + } + } + console.log(''); // Add spacing before summary // Get commit SHA for summary @@ -200,7 +262,9 @@ export const run = async (argvArg: any) => { commitMessage: answerBucket.getAnswerFor('commitDescription'), newVersion: newVersion, commitSha: commitSha, - pushed: answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'), + pushed: willPush, + released: releasedRegistries.length > 0, + releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined, }); }; diff --git a/ts/mod_commit/mod.ui.ts b/ts/mod_commit/mod.ui.ts index 35f92aa..1205d97 100644 --- a/ts/mod_commit/mod.ui.ts +++ b/ts/mod_commit/mod.ui.ts @@ -14,6 +14,8 @@ interface ICommitSummary { commitSha?: string; pushed: boolean; repoUrl?: string; + released?: boolean; + releasedRegistries?: string[]; } interface IRecommendation { @@ -146,6 +148,13 @@ export function printSummary(summary: ICommitSummary): void { lines.push(`Remote: ⊘ Not pushed (local only)`); } + if (summary.released && summary.releasedRegistries && summary.releasedRegistries.length > 0) { + lines.push(`Published: ✓ Released to ${summary.releasedRegistries.length} registr${summary.releasedRegistries.length === 1 ? 'y' : 'ies'}`); + summary.releasedRegistries.forEach((registry) => { + lines.push(` → ${registry}`); + }); + } + if (summary.repoUrl && summary.commitSha) { lines.push(''); lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`); @@ -153,7 +162,9 @@ export function printSummary(summary: ICommitSummary): void { printSection('✅ Commit Summary', lines); - if (summary.pushed) { + if (summary.released) { + console.log('🎉 All done! Your changes are committed, pushed, and released.\n'); + } else if (summary.pushed) { console.log('🎉 All done! Your changes are committed and pushed.\n'); } else { console.log('✓ Commit created successfully.\n'); diff --git a/ts/mod_config/classes.releaseconfig.ts b/ts/mod_config/classes.releaseconfig.ts new file mode 100644 index 0000000..a1ff72b --- /dev/null +++ b/ts/mod_config/classes.releaseconfig.ts @@ -0,0 +1,144 @@ +import * as plugins from './mod.plugins.js'; + +export interface IReleaseConfig { + registries: string[]; +} + +/** + * Manages release configuration stored in npmextra.json + * under @git.zone/cli.release namespace + */ +export class ReleaseConfig { + private cwd: string; + private config: IReleaseConfig; + + constructor(cwd: string = process.cwd()) { + this.cwd = cwd; + this.config = { registries: [] }; + } + + /** + * Create a ReleaseConfig instance from current working directory + */ + public static async fromCwd(cwd: string = process.cwd()): Promise { + const instance = new ReleaseConfig(cwd); + await instance.load(); + return instance; + } + + /** + * Load configuration from npmextra.json + */ + public async load(): Promise { + const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd); + const gitzoneConfig = npmextraInstance.dataFor('@git.zone/cli', {}); + + this.config = { + registries: gitzoneConfig?.release?.registries || [], + }; + } + + /** + * Save configuration to npmextra.json + */ + public async save(): Promise { + const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json'); + let npmextraData: any = {}; + + // Read existing npmextra.json + if (await plugins.smartfs.file(npmextraPath).exists()) { + const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read(); + npmextraData = JSON.parse(content as string); + } + + // Ensure @git.zone/cli namespace exists + if (!npmextraData['@git.zone/cli']) { + npmextraData['@git.zone/cli'] = {}; + } + + // Ensure release object exists + if (!npmextraData['@git.zone/cli'].release) { + npmextraData['@git.zone/cli'].release = {}; + } + + // Update registries + npmextraData['@git.zone/cli'].release.registries = this.config.registries; + + // Write back to file + await plugins.smartfs + .file(npmextraPath) + .encoding('utf8') + .write(JSON.stringify(npmextraData, null, 2)); + } + + /** + * Get all configured registries + */ + public getRegistries(): string[] { + return [...this.config.registries]; + } + + /** + * Check if any registries are configured + */ + public hasRegistries(): boolean { + return this.config.registries.length > 0; + } + + /** + * Add a registry URL + * @returns true if added, false if already exists + */ + public addRegistry(url: string): boolean { + const normalizedUrl = this.normalizeUrl(url); + + if (this.config.registries.includes(normalizedUrl)) { + return false; + } + + this.config.registries.push(normalizedUrl); + return true; + } + + /** + * Remove a registry URL + * @returns true if removed, false if not found + */ + public removeRegistry(url: string): boolean { + const normalizedUrl = this.normalizeUrl(url); + const index = this.config.registries.indexOf(normalizedUrl); + + if (index === -1) { + return false; + } + + this.config.registries.splice(index, 1); + return true; + } + + /** + * Clear all registries + */ + public clearRegistries(): void { + this.config.registries = []; + } + + /** + * Normalize a registry URL (ensure it has https:// prefix) + */ + private normalizeUrl(url: string): string { + let normalized = url.trim(); + + // Add https:// if no protocol specified + if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) { + normalized = `https://${normalized}`; + } + + // Remove trailing slash + if (normalized.endsWith('/')) { + normalized = normalized.slice(0, -1); + } + + return normalized; + } +} diff --git a/ts/mod_config/index.ts b/ts/mod_config/index.ts new file mode 100644 index 0000000..dfb36bb --- /dev/null +++ b/ts/mod_config/index.ts @@ -0,0 +1,169 @@ +// gitzone config - manage release registry configuration + +import * as plugins from './mod.plugins.js'; +import { ReleaseConfig } from './classes.releaseconfig.js'; + +export { ReleaseConfig }; + +export const run = async (argvArg: any) => { + const command = argvArg._?.[1] || 'show'; + const value = argvArg._?.[2]; + + switch (command) { + case 'show': + await handleShow(); + break; + case 'add': + await handleAdd(value); + break; + case 'remove': + await handleRemove(value); + break; + case 'clear': + await handleClear(); + break; + default: + showHelp(); + } +}; + +/** + * Show current registry configuration + */ +async function handleShow(): Promise { + const config = await ReleaseConfig.fromCwd(); + const registries = config.getRegistries(); + + console.log(''); + console.log('╭─────────────────────────────────────────────────────────────╮'); + console.log('│ Release Registry Configuration │'); + console.log('╰─────────────────────────────────────────────────────────────╯'); + 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): Promise { + if (!url) { + // 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(); + plugins.logger.log('success', `Added registry: ${url}`); + } else { + plugins.logger.log('warn', `Registry already exists: ${url}`); + } +} + +/** + * Remove a registry URL + */ +async function handleRemove(url?: string): 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) { + // 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(); + plugins.logger.log('success', `Removed registry: ${url}`); + } else { + plugins.logger.log('warn', `Registry not found: ${url}`); + } +} + +/** + * Clear all registries + */ +async function handleClear(): Promise { + const config = await ReleaseConfig.fromCwd(); + + if (!config.hasRegistries()) { + plugins.logger.log('info', 'No registries to clear.'); + return; + } + + // Confirm before clearing + const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation( + 'Clear all configured registries?', + false + ); + + if (confirmed) { + config.clearRegistries(); + await config.save(); + plugins.logger.log('success', 'All registries cleared.'); + } else { + plugins.logger.log('info', 'Operation cancelled.'); + } +} + +/** + * Show help for config command + */ +function showHelp(): void { + console.log(''); + console.log('Usage: gitzone config [options]'); + console.log(''); + console.log('Commands:'); + console.log(' show Display current registry configuration'); + console.log(' add [url] Add a registry URL'); + console.log(' remove [url] Remove a registry URL'); + console.log(' clear Clear all registries'); + console.log(''); + console.log('Examples:'); + console.log(' gitzone config show'); + 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(''); +} diff --git a/ts/mod_config/mod.plugins.ts b/ts/mod_config/mod.plugins.ts new file mode 100644 index 0000000..4cbf5db --- /dev/null +++ b/ts/mod_config/mod.plugins.ts @@ -0,0 +1,3 @@ +// mod_config plugins +export * from '../plugins.js'; +export { logger } from '../gitzone.logging.js';