Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ fileignoreconfig:
- filename: packages/contentstack-audit/src/modules/workflows.ts
checksum: 20d1f1985ea2657d3f9fc41b565a44000cbda47e2a60a576fee2aaff06f49352
- filename: packages/contentstack-audit/src/modules/field_rules.ts
checksum: 3eaca968126c9e0e12115491f7942341124c9962d5285dd1cfb355d9e60c6106
checksum: f3ec8f44f8dd73601aa8da1207a72335faf0a12d52e792c1da90ba1bdeef38a7
- filename: packages/contentstack-audit/src/modules/entries.ts
checksum: 305af34194771343fee4e1d4bef60d065f1b8d1d8c1059a332f5d6c52e637ff1
checksum: d8b6aa896aef2a9846f4dbde066d74d5b1e7b5cdbb8b548989616f9af7a8d26b
- filename: packages/contentstack-audit/test/unit/base-command.test.ts
checksum: b0fa8088fcbb17510fa275bd0dde3f6f4246f2525741c30426f07dd62fe497b0
- filename: packages/contentstack-audit/src/modules/content-types.ts
checksum: ddf7b08e6a80af09c6a7019a637c26089fb76572c7c3d079a8af244b02985f16
checksum: e325a50db567abc5d0de758767037dbc10bb76501aadda32999bc96e17595d1b
- filename: packages/contentstack-import/test/unit/commands/cm/stacks/import.test.ts
checksum: b11e57f1b824d405f86438e9e7c59183f8c59b66b42d8d16dbeaf76195a30548
- filename: packages/contentstack-import/test/unit/utils/asset-helper.test.ts
Expand Down
95 changes: 66 additions & 29 deletions packages/contentstack-audit/src/audit-base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid';
import isEmpty from 'lodash/isEmpty';
import { join, resolve } from 'path';
import cloneDeep from 'lodash/cloneDeep';
import { cliux, sanitizePath, TableFlags, TableHeader, log, configHandler } from '@contentstack/cli-utilities';
import { cliux, sanitizePath, TableFlags, TableHeader, log, configHandler, CLIProgressManager, clearProgressModuleSetting } from '@contentstack/cli-utilities';
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
import config from './config';
import { print } from './util/log';
Expand Down Expand Up @@ -71,11 +71,23 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
*/
async start(command: CommandNames): Promise<boolean> {
this.currentCommand = command;

// Set progress supported module and console logs setting BEFORE any log calls
// This ensures the logger respects the setting when it's initialized
const logConfig = configHandler.get('log') || {};
// Default to false so progress bars are shown instead of console logs
if (logConfig.showConsoleLogs === undefined) {
configHandler.set('log.showConsoleLogs', false);
}
configHandler.set('log.progressSupportedModule', 'audit');

// Initialize audit context
this.auditContext = this.createAuditContext();
log.debug(`Starting audit command: ${command}`, this.auditContext);
log.info(`Starting audit command: ${command}`, this.auditContext);


// Initialize global summary for progress tracking
CLIProgressManager.initializeGlobalSummary('AUDIT', '', 'Auditing content...');

await this.promptQueue();
await this.createBackUp();
Expand Down Expand Up @@ -163,6 +175,12 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
}
}

// Print comprehensive summary at the end
CLIProgressManager.printGlobalSummary();

// Clear progress module setting now that audit is complete
clearProgressModuleSetting();

return (
!isEmpty(missingCtRefs) ||
!isEmpty(missingGfRefs) ||
Expand Down Expand Up @@ -222,47 +240,59 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma

let dataModuleWise: Record<string, any> = await new ModuleDataReader(cloneDeep(constructorParam)).run();
log.debug(`Data module wise: ${JSON.stringify(dataModuleWise)}`, this.auditContext);

// Extract logConfig and showConsoleLogs once before the loop to reuse throughout
const logConfig = configHandler.get('log') || {};
const showConsoleLogs = logConfig.showConsoleLogs ?? true;

for (const module of this.sharedConfig.flags.modules || this.sharedConfig.modules) {
// Update audit context with current module
this.auditContext = this.createAuditContext(module);
log.debug(`Starting audit for module: ${module}`, this.auditContext);
log.info(`Starting audit for module: ${module}`, this.auditContext);

print([
{
bold: true,
color: 'whiteBright',
message: this.$t(this.messages.AUDIT_START_SPINNER, { module }),
},
]);
// Only show spinner message if console logs are enabled (compatible with line-by-line logs)
if (showConsoleLogs) {
print([
{
bold: true,
color: 'whiteBright',
message: this.$t(this.messages.AUDIT_START_SPINNER, { module }),
},
]);
}

constructorParam['moduleName'] = module;

switch (module) {
case 'assets':
log.info('Executing assets audit', this.auditContext);
missingEnvLocalesInAssets = await new Assets(cloneDeep(constructorParam)).run();
const assetsTotalCount = dataModuleWise['assets']?.Total || 0;
missingEnvLocalesInAssets = await new Assets(cloneDeep(constructorParam)).run(false, assetsTotalCount);
await this.prepareReport(module, missingEnvLocalesInAssets);
this.getAffectedData('assets', dataModuleWise['assets'], missingEnvLocalesInAssets);
log.success(`Assets audit completed. Found ${Object.keys(missingEnvLocalesInAssets || {}).length} issues`, this.auditContext);
break;
case 'content-types':
log.info('Executing content-types audit', this.auditContext);
missingCtRefs = await new ContentType(cloneDeep(constructorParam)).run();
const contentTypesTotalCount = dataModuleWise['content-types']?.Total || 0;
missingCtRefs = await new ContentType(cloneDeep(constructorParam)).run(false, contentTypesTotalCount);
await this.prepareReport(module, missingCtRefs);
this.getAffectedData('content-types', dataModuleWise['content-types'], missingCtRefs);
log.success(`Content-types audit completed. Found ${Object.keys(missingCtRefs || {}).length} issues`, this.auditContext);
break;
case 'global-fields':
log.info('Executing global-fields audit', this.auditContext);
missingGfRefs = await new GlobalField(cloneDeep(constructorParam)).run();
const globalFieldsTotalCount = dataModuleWise['global-fields']?.Total || 0;
missingGfRefs = await new GlobalField(cloneDeep(constructorParam)).run(false, globalFieldsTotalCount);
await this.prepareReport(module, missingGfRefs);
this.getAffectedData('global-fields', dataModuleWise['global-fields'], missingGfRefs);
log.success(`Global-fields audit completed. Found ${Object.keys(missingGfRefs || {}).length} issues`, this.auditContext);
break;
case 'entries':
log.info('Executing entries audit', this.auditContext);
missingEntry = await new Entries(cloneDeep(constructorParam)).run();
const entriesTotalCount = dataModuleWise['entries']?.Total || 0;
missingEntry = await new Entries(cloneDeep(constructorParam)).run(entriesTotalCount);
missingEntryRefs = missingEntry.missingEntryRefs ?? {};
missingSelectFeild = missingEntry.missingSelectFeild ?? {};
missingMandatoryFields = missingEntry.missingMandatoryFields ?? {};
Expand All @@ -286,27 +316,30 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
break;
case 'workflows':
log.info('Executing workflows audit', this.auditContext);
const workflowsTotalCount = dataModuleWise['workflows']?.Total || 0;
missingCtRefsInWorkflow = await new Workflows({
ctSchema,
moduleName: module,
config: this.sharedConfig,
fix: this.currentCommand === 'cm:stacks:audit:fix',
}).run();
}).run(workflowsTotalCount);
await this.prepareReport(module, missingCtRefsInWorkflow);
this.getAffectedData('workflows', dataModuleWise['workflows'], missingCtRefsInWorkflow);
log.success(`Workflows audit completed. Found ${Object.keys(missingCtRefsInWorkflow || {}).length} issues`, this.auditContext);

break;
case 'extensions':
log.info('Executing extensions audit', this.auditContext);
missingCtRefsInExtensions = await new Extensions(cloneDeep(constructorParam)).run();
const extensionsTotalCount = dataModuleWise['extensions']?.Total || 0;
missingCtRefsInExtensions = await new Extensions(cloneDeep(constructorParam)).run(extensionsTotalCount);
await this.prepareReport(module, missingCtRefsInExtensions);
this.getAffectedData('extensions', dataModuleWise['extensions'], missingCtRefsInExtensions);
log.success(`Extensions audit completed. Found ${Object.keys(missingCtRefsInExtensions || {}).length} issues`, this.auditContext);
break;
case 'custom-roles':
log.info('Executing custom-roles audit', this.auditContext);
missingRefInCustomRoles = await new CustomRoles(cloneDeep(constructorParam)).run();
const customRolesTotalCount = dataModuleWise['custom-roles']?.Total || 0;
missingRefInCustomRoles = await new CustomRoles(cloneDeep(constructorParam)).run(customRolesTotalCount);
await this.prepareReport(module, missingRefInCustomRoles);
this.getAffectedData('custom-roles', dataModuleWise['custom-roles'], missingRefInCustomRoles);
log.success(`Custom-roles audit completed. Found ${Object.keys(missingRefInCustomRoles || {}).length} issues`, this.auditContext);
Expand All @@ -318,25 +351,29 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
const data = this.getCtAndGfSchema();
constructorParam.ctSchema = data.ctSchema;
constructorParam.gfSchema = data.gfSchema;
missingFieldRules = await new FieldRule(cloneDeep(constructorParam)).run();
const fieldRulesTotalCount = dataModuleWise['content-types']?.Total || 0;
missingFieldRules = await new FieldRule(cloneDeep(constructorParam)).run(fieldRulesTotalCount);
await this.prepareReport(module, missingFieldRules);
this.getAffectedData('field-rules', dataModuleWise['content-types'], missingFieldRules);
log.success(`Field-rules audit completed. Found ${Object.keys(missingFieldRules || {}).length} issues`, this.auditContext);
break;
}

print([
{
bold: true,
color: 'whiteBright',
message: this.$t(this.messages.AUDIT_START_SPINNER, { module }),
},
{
bold: true,
message: ' done',
color: 'whiteBright',
},
]);
// Only show completion message if console logs are enabled
if (showConsoleLogs) {
print([
{
bold: true,
color: 'whiteBright',
message: this.$t(this.messages.AUDIT_START_SPINNER, { module }),
},
{
bold: true,
message: ' done',
color: 'whiteBright',
},
]);
}
}

log.debug('Scan and fix process completed', this.auditContext);
Expand Down
69 changes: 45 additions & 24 deletions packages/contentstack-audit/src/modules/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { join, resolve } from 'path';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { FsUtility, sanitizePath, cliux, log } from '@contentstack/cli-utilities';
import {
ConfigType,
ContentTypeStruct,
CtConstructorParam,
ModuleConstructorParam,
Expand All @@ -12,13 +11,13 @@ import auditConfig from '../config';
import { $t, auditFixMsg, auditMsg, commonMsg } from '../messages';
import values from 'lodash/values';
import { keys } from 'lodash';
import BaseClass from './base-class';

/* The `ContentType` class is responsible for scanning content types, looking for references, and
generating a report in JSON and CSV formats. */
export default class Assets {
/* The `Assets` class is responsible for scanning assets, looking for missing environment/locale references,
and generating a report in JSON and CSV formats. */
export default class Assets extends BaseClass {
protected fix: boolean;
public fileName: string;
public config: ConfigType;
public folderPath: string;
public currentUid!: string;
public currentTitle!: string;
Expand All @@ -30,7 +29,7 @@ export default class Assets {
public moduleName: keyof typeof auditConfig.moduleConfig;

constructor({ fix, config, moduleName }: ModuleConstructorParam & CtConstructorParam) {
this.config = config;
super({ config });
this.fix = fix ?? false;
this.moduleName = this.validateModules(moduleName!, this.config.moduleConfig);
this.fileName = config.moduleConfig[this.moduleName].fileName;
Expand All @@ -52,25 +51,36 @@ export default class Assets {
/**
* The `run` function checks if a folder path exists, sets the schema based on the module name,
* iterates over the schema and looks for references, and returns a list of missing references.
* @param returnFixSchema - If true, returns the fixed schema instead of missing references
* @param totalCount - Total number of assets to process (for progress tracking)
* @returns the `missingEnvLocales` object.
*/
async run(returnFixSchema = false) {
log.debug(`Starting ${this.moduleName} audit process`, this.config.auditContext);
log.debug(`Data directory: ${this.folderPath}`, this.config.auditContext);
log.debug(`Fix mode: ${this.fix}`, this.config.auditContext);

if (!existsSync(this.folderPath)) {
log.debug(`Skipping ${this.moduleName} audit - path does not exist`, this.config.auditContext);
log.warn(`Skipping ${this.moduleName} audit`, this.config.auditContext);
cliux.print($t(auditMsg.NOT_VALID_PATH, { path: this.folderPath }), { color: 'yellow' });
return returnFixSchema ? [] : {};
}
async run(returnFixSchema = false, totalCount?: number) {
try {
log.debug(`Starting ${this.moduleName} audit process`, this.config.auditContext);
log.debug(`Data directory: ${this.folderPath}`, this.config.auditContext);
log.debug(`Fix mode: ${this.fix}`, this.config.auditContext);

log.debug('Loading prerequisite data (locales and environments)', this.config.auditContext);
await this.prerequisiteData();
if (!existsSync(this.folderPath)) {
log.debug(`Skipping ${this.moduleName} audit - path does not exist`, this.config.auditContext);
log.warn(`Skipping ${this.moduleName} audit`, this.config.auditContext);
cliux.print($t(auditMsg.NOT_VALID_PATH, { path: this.folderPath }), { color: 'yellow' });
return returnFixSchema ? [] : {};
}

// Load prerequisite data with loading spinner
await this.withLoadingSpinner('ASSETS: Loading prerequisite data (locales and environments)...', async () => {
await this.prerequisiteData();
});

// Create progress manager if we have a total count
if (totalCount && totalCount > 0) {
const progress = this.createSimpleProgress(this.moduleName, totalCount);
progress.updateStatus('Validating asset references...');
}

log.debug('Starting asset Reference, Environment and Locale validation', this.config.auditContext);
await this.lookForReference();
log.debug('Starting asset Reference, Environment and Locale validation', this.config.auditContext);
await this.lookForReference();

if (returnFixSchema) {
log.debug(`Returning fixed schema with ${this.schema?.length || 0} items`, this.config.auditContext);
Expand All @@ -86,9 +96,15 @@ export default class Assets {
}
}

const totalIssues = Object.keys(this.missingEnvLocales).length;
log.debug(`${this.moduleName} audit completed. Found ${totalIssues} assets with missing environment/locale references`, this.config.auditContext);
return this.missingEnvLocales;
const totalIssues = Object.keys(this.missingEnvLocales).length;
log.debug(`${this.moduleName} audit completed. Found ${totalIssues} assets with missing environment/locale references`, this.config.auditContext);

this.completeProgress(true);
return this.missingEnvLocales;
} catch (error: any) {
this.completeProgress(false, error?.message || 'Assets audit failed');
throw error;
}
}

/**
Expand Down Expand Up @@ -227,6 +243,11 @@ export default class Assets {
const remainingPublishDetails = this.assets[assetUid].publish_details?.length || 0;
log.debug(`Asset ${assetUid} now has ${remainingPublishDetails} valid publish details`, this.config.auditContext);

// Track progress for each asset processed
if (this.progressManager) {
this.progressManager.tick(true, `asset: ${assetUid}`, null);
}

if (this.fix) {
log.debug(`Fixing asset ${assetUid}`, this.config.auditContext);
log.info($t(auditFixMsg.ASSET_FIX, { uid: assetUid }), this.config.auditContext);
Expand Down
57 changes: 57 additions & 0 deletions packages/contentstack-audit/src/modules/base-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { CLIProgressManager, configHandler } from '@contentstack/cli-utilities';
import { ConfigType, ModuleConstructorParam } from '../types';

export default abstract class BaseClass {
protected progressManager: CLIProgressManager | null = null;
protected currentModuleName: string = '';
public config: ConfigType;

constructor({ config }: ModuleConstructorParam) {
this.config = config;
}

/**
* Create simple progress manager
*/
protected createSimpleProgress(moduleName: string, total?: number): CLIProgressManager {
this.currentModuleName = moduleName;
const logConfig = configHandler.get('log') || {};
const showConsoleLogs = logConfig.showConsoleLogs ?? false;
this.progressManager = CLIProgressManager.createSimple(moduleName, total, showConsoleLogs);
return this.progressManager;
}

/**
* Create nested progress manager
*/
protected createNestedProgress(moduleName: string): CLIProgressManager {
this.currentModuleName = moduleName;
const logConfig = configHandler.get('log') || {};
const showConsoleLogs = logConfig.showConsoleLogs ?? false;
this.progressManager = CLIProgressManager.createNested(moduleName, showConsoleLogs);
return this.progressManager;
}

/**
* Complete progress manager
*/
protected completeProgress(success: boolean = true, error?: string): void {
this.progressManager?.complete(success, error);
this.progressManager = null;
}

/**
* Execute action with loading spinner (if console logs are disabled)
*/
protected async withLoadingSpinner<T>(message: string, action: () => Promise<T>): Promise<T> {
const logConfig = configHandler.get('log') || {};
const showConsoleLogs = logConfig.showConsoleLogs ?? false;

if (showConsoleLogs) {
// If console logs are enabled, don't show spinner, just execute the action
return await action();
}
return await CLIProgressManager.withLoadingSpinner(message, action);
}
}

Loading
Loading