Files
cli/ts/mod_format/formatters/smartconfig.formatter.ts
2026-03-24 16:10:51 +00:00

214 lines
6.2 KiB
TypeScript

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<IPlannedChange[]> {
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<void> {
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}`);
}
}