import { BaseFormatter } from '../classes.baseformatter.js'; import type { IPlannedChange } from '../interfaces.format.js'; import * as plugins from '../mod.plugins.js'; import { logger, logVerbose } from '../../gitzone.logging.js'; /** * Migrates .smartconfig.json from old namespace keys to new package-scoped keys */ const migrateNamespaceKeys = (smartconfigJson: any): boolean => { let migrated = false; const migrations = [ { oldKey: 'gitzone', newKey: '@git.zone/cli' }, { oldKey: 'tsdoc', newKey: '@git.zone/tsdoc' }, { oldKey: 'npmdocker', newKey: '@git.zone/tsdocker' }, { oldKey: 'npmci', newKey: '@ship.zone/szci' }, { oldKey: 'szci', newKey: '@ship.zone/szci' }, ]; for (const { oldKey, newKey } of migrations) { if (smartconfigJson[oldKey]) { if (!smartconfigJson[newKey]) { smartconfigJson[newKey] = smartconfigJson[oldKey]; } else { smartconfigJson[newKey] = { ...smartconfigJson[oldKey], ...smartconfigJson[newKey], }; } delete smartconfigJson[oldKey]; migrated = true; } } return migrated; }; /** * Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel */ const migrateAccessLevel = (smartconfigJson: any): boolean => { const szciConfig = smartconfigJson['@ship.zone/szci']; if (!szciConfig?.npmAccessLevel) { return false; } const gitzoneConfig = smartconfigJson['@git.zone/cli'] || {}; if (gitzoneConfig?.release?.accessLevel) { delete szciConfig.npmAccessLevel; return true; } if (!smartconfigJson['@git.zone/cli']) { smartconfigJson['@git.zone/cli'] = {}; } if (!smartconfigJson['@git.zone/cli'].release) { smartconfigJson['@git.zone/cli'].release = {}; } smartconfigJson['@git.zone/cli'].release.accessLevel = szciConfig.npmAccessLevel; delete szciConfig.npmAccessLevel; return true; }; // Config file names in priority order (newest → oldest) const CONFIG_FILE_NAMES = ['.smartconfig.json', 'smartconfig.json', 'npmextra.json']; const TARGET_CONFIG_FILE = '.smartconfig.json'; export class SmartconfigFormatter extends BaseFormatter { get name(): string { return 'smartconfig'; } /** * Find the config file, checking in priority order. * Returns the path and whether it needs renaming. */ private async findConfigFile(): Promise<{ path: string; needsRename: boolean } | null> { for (const filename of CONFIG_FILE_NAMES) { const exists = await plugins.smartfs.file(filename).exists(); if (exists) { return { path: filename, needsRename: filename !== TARGET_CONFIG_FILE, }; } } return null; } async analyze(): Promise { const changes: IPlannedChange[] = []; const configFile = await this.findConfigFile(); if (!configFile) { logVerbose('No config file found (.smartconfig.json, smartconfig.json, or npmextra.json), skipping'); return changes; } // Read current content const currentContent = (await plugins.smartfs .file(configFile.path) .encoding('utf8') .read()) as string; // Parse and apply migrations const smartconfigJson = JSON.parse(currentContent); migrateNamespaceKeys(smartconfigJson); migrateAccessLevel(smartconfigJson); // Ensure namespaces exist if (!smartconfigJson['@git.zone/cli']) { smartconfigJson['@git.zone/cli'] = {}; } if (!smartconfigJson['@ship.zone/szci']) { smartconfigJson['@ship.zone/szci'] = {}; } const newContent = JSON.stringify(smartconfigJson, null, 2); // If file needs renaming, plan a create + delete if (configFile.needsRename) { changes.push({ type: 'create', path: TARGET_CONFIG_FILE, module: this.name, description: `Migrate ${configFile.path} to ${TARGET_CONFIG_FILE}`, content: newContent, }); changes.push({ type: 'delete', path: configFile.path, module: this.name, description: `Remove old ${configFile.path}`, }); } else if (newContent !== currentContent) { // File is already .smartconfig.json, just needs content update changes.push({ type: 'modify', path: TARGET_CONFIG_FILE, module: this.name, description: 'Migrate and format .smartconfig.json', content: newContent, }); } return changes; } async applyChange(change: IPlannedChange): Promise { if (change.type === 'delete') { await this.deleteFile(change.path); logger.log('info', `Removed old config file ${change.path}`); return; } if (!change.content) return; // Parse the content to check for missing required fields const smartconfigJson = JSON.parse(change.content); const expectedRepoInformation: string[] = [ 'projectType', 'module.githost', 'module.gitscope', 'module.gitrepo', 'module.description', 'module.npmPackagename', 'module.license', ]; const interactInstance = new plugins.smartinteract.SmartInteract(); for (const expectedRepoInformationItem of expectedRepoInformation) { if ( !plugins.smartobject.smartGet( smartconfigJson['@git.zone/cli'], expectedRepoInformationItem, ) ) { interactInstance.addQuestions([ { message: `What is the value of ${expectedRepoInformationItem}`, name: expectedRepoInformationItem, type: 'input', default: 'undefined variable', }, ]); } } const answerbucket = await interactInstance.runQueue(); for (const expectedRepoInformationItem of expectedRepoInformation) { const cliProvidedValue = answerbucket.getAnswerFor( expectedRepoInformationItem, ); if (cliProvidedValue) { plugins.smartobject.smartAdd( smartconfigJson['@git.zone/cli'], expectedRepoInformationItem, cliProvidedValue, ); } } const finalContent = JSON.stringify(smartconfigJson, null, 2); if (change.type === 'create') { await this.createFile(change.path, finalContent); } else { await this.modifyFile(change.path, finalContent); } logger.log('info', `Updated ${change.path}`); } }