This commit is contained in:
2025-12-14 01:31:06 +00:00
parent 106b72748c
commit 48c4b0c9b2
6 changed files with 407 additions and 8 deletions

View File

@@ -131,6 +131,14 @@ export let run = async () => {
modHelpers.run(argvArg); 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) * manage development services (MongoDB, S3/MinIO)
*/ */

View File

@@ -5,8 +5,24 @@ import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js'; import { logger } from '../gitzone.logging.js';
import * as helpers from './mod.helpers.js'; import * as helpers from './mod.helpers.js';
import * as ui from './mod.ui.js'; import * as ui from './mod.ui.js';
import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js';
export const run = async (argvArg: any) => { 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 <registry-url>` to add registries.');
console.log('');
process.exit(1);
}
}
if (argvArg.format) { if (argvArg.format) {
const formatMod = await import('../mod_format/index.js'); const formatMod = await import('../mod_format/index.js');
await formatMod.run(); await formatMod.run();
@@ -56,6 +72,10 @@ export const run = async (argvArg: any) => {
name: 'pushToOrigin', name: 'pushToOrigin',
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
}); });
answerBucket.addAnswer({
name: 'createRelease',
value: wantsRelease,
});
} else { } else {
// Warn if --yes was provided but we're requiring confirmation due to breaking change // Warn if --yes was provided but we're requiring confirmation due to breaking change
if (isBreakingChange && (argvArg.y || argvArg.yes)) { 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?`, message: `Do you want to push this version now?`,
default: true, default: true,
}, },
{
type: 'confirm',
name: `createRelease`,
message: `Do you want to publish to npm registries?`,
default: wantsRelease,
},
]); ]);
answerBucket = await commitInteract.runQueue(); answerBucket = await commitInteract.runQueue();
} }
@@ -111,8 +137,24 @@ export const run = async (argvArg: any) => {
sourceFilePaths: [], sourceFilePaths: [],
}); });
// Determine total steps (6 if pushing, 5 if not) // Load release config if user wants to release (interactively selected)
const totalSteps = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true') ? 6 : 5; 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 <registry-url>` 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; let currentStep = 0;
// Step 1: Baking commitinfo // Step 1: Baking commitinfo
@@ -175,16 +217,36 @@ export const run = async (argvArg: any) => {
// Step 6: Push to remote (optional) // Step 6: Push to remote (optional)
const currentBranch = await helpers.detectCurrentBranch(); const currentBranch = await helpers.detectCurrentBranch();
if ( if (willPush) {
answerBucket.getAnswerFor('pushToOrigin') &&
!(process.env.CI === 'true')
) {
currentStep++; currentStep++;
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress'); ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress');
await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`); await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`);
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'done'); 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 console.log(''); // Add spacing before summary
// Get commit SHA for summary // Get commit SHA for summary
@@ -200,7 +262,9 @@ export const run = async (argvArg: any) => {
commitMessage: answerBucket.getAnswerFor('commitDescription'), commitMessage: answerBucket.getAnswerFor('commitDescription'),
newVersion: newVersion, newVersion: newVersion,
commitSha: commitSha, commitSha: commitSha,
pushed: answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'), pushed: willPush,
released: releasedRegistries.length > 0,
releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined,
}); });
}; };

View File

@@ -14,6 +14,8 @@ interface ICommitSummary {
commitSha?: string; commitSha?: string;
pushed: boolean; pushed: boolean;
repoUrl?: string; repoUrl?: string;
released?: boolean;
releasedRegistries?: string[];
} }
interface IRecommendation { interface IRecommendation {
@@ -146,6 +148,13 @@ export function printSummary(summary: ICommitSummary): void {
lines.push(`Remote: ⊘ Not pushed (local only)`); 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) { if (summary.repoUrl && summary.commitSha) {
lines.push(''); lines.push('');
lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`); lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`);
@@ -153,7 +162,9 @@ export function printSummary(summary: ICommitSummary): void {
printSection('✅ Commit Summary', lines); 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'); console.log('🎉 All done! Your changes are committed and pushed.\n');
} else { } else {
console.log('✓ Commit created successfully.\n'); console.log('✓ Commit created successfully.\n');

View File

@@ -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<ReleaseConfig> {
const instance = new ReleaseConfig(cwd);
await instance.load();
return instance;
}
/**
* Load configuration from npmextra.json
*/
public async load(): Promise<void> {
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
this.config = {
registries: gitzoneConfig?.release?.registries || [],
};
}
/**
* Save configuration to npmextra.json
*/
public async save(): Promise<void> {
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;
}
}

169
ts/mod_config/index.ts Normal file
View File

@@ -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<void> {
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 <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): Promise<void> {
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<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) {
// 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<void> {
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 <command> [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('');
}

View File

@@ -0,0 +1,3 @@
// mod_config plugins
export * from '../plugins.js';
export { logger } from '../gitzone.logging.js';