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: 6 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ fileignoreconfig:
version: '1.0'
- filename: .github/workflows/release-production-pipeline.yml
checksum: dd858a2c2a3297c5651c2843ddae73ad0776a0386329200d01b051ddc489871e
- filename: packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts
checksum: be7e833dc0abbbb211dc71f7970d8a1c680860f978dc2b871bcc9a30a659f436
- filename: packages/contentstack-utilities/test/unit/logger.test.ts
checksum: ca0bbc2838a0b6a069d8fc4874e465dd30dd35377a6202eb005eb43e5ff5d871
- filename: packages/contentstack-utilities/test/unit/cliProgressManager.test.ts
checksum: 6df6c21e1188b077a83a0464aecc4c628941a3fbb3bdea99db18d30b5bd3cf71
9 changes: 9 additions & 0 deletions packages/contentstack-auth/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
CLIError,
authHandler as oauthHandler,
cliux,
flags,
managementSDKClient,
FlagInput,
log,
handleAndLogError,
isConsoleLogEnabled,
messageHandler,
} from '@contentstack/cli-utilities';
import { User } from '../../interfaces';
Expand Down Expand Up @@ -132,6 +134,13 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
log.debug('Configuration data set successfully.', this.contextDetails);

log.success(messageHandler.parse('CLI_AUTH_LOGIN_SUCCESS'), this.contextDetails);

// log.success maps to the info level, and the Console transport is suppressed for every
// level when the console-log policy is off — so this is the command's only success
// output and it reaches nobody. auth has no progress UI to interleave with.
if (!isConsoleLogEnabled()) {
cliux.success('CLI_AUTH_LOGIN_SUCCESS');
}
log.debug('Login completed successfully.', this.contextDetails);
} catch (error) {
log.debug('Login failed.', { ...this.contextDetails, error });
Expand Down
12 changes: 12 additions & 0 deletions packages/contentstack-auth/src/commands/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FlagInput,
log,
handleAndLogError,
isConsoleLogEnabled,
messageHandler,
} from '@contentstack/cli-utilities';

Expand Down Expand Up @@ -77,6 +78,12 @@ export default class LogoutCommand extends BaseCommand<typeof LogoutCommand> {

cliux.loader('');
log.success(messageHandler.parse('CLI_AUTH_LOGOUT_SUCCESS'), this.contextDetails);

// Same as auth:login — the only success output, and dark when the console-log policy
// is off. The loader is stopped on the line above, so nothing is rendering here.
if (!isConsoleLogEnabled()) {
cliux.success('CLI_AUTH_LOGOUT_SUCCESS');
}
log.debug('Logout completed successfully.', this.contextDetails);
} else {
log.debug('User not confirmed or not authenticated, skipping logout', {
Expand All @@ -88,6 +95,11 @@ export default class LogoutCommand extends BaseCommand<typeof LogoutCommand> {
? 'CLI_AUTH_LOGOUT_CANCELLED'
: 'CLI_AUTH_LOGOUT_ALREADY';
log.success(messageHandler.parse(messageKey), this.contextDetails);

// Cancelled / already-logged-out: without this the user answers "no" and sees nothing.
if (!isConsoleLogEnabled()) {
cliux.success(messageKey);
}
}
} catch (error) {
log.debug('Logout failed.', { ...this.contextDetails, error: error.message });
Expand Down
9 changes: 8 additions & 1 deletion packages/contentstack-auth/src/commands/auth/whoami.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cliux, log, handleAndLogError, messageHandler } from '@contentstack/cli-utilities';
import { cliux, log, handleAndLogError, isConsoleLogEnabled, messageHandler } from '@contentstack/cli-utilities';
import { BaseCommand } from '../../base-command';

export default class WhoamiCommand extends BaseCommand<typeof WhoamiCommand> {
Expand All @@ -22,6 +22,13 @@ export default class WhoamiCommand extends BaseCommand<typeof WhoamiCommand> {
} else {
log.debug('No user email found in context.', this.contextDetails);
log.error(messageHandler.parse('CLI_AUTH_WHOAMI_FAILED'), this.contextDetails);

// Not-logged-in is the whole answer this command exists to give, and log.error is
// suppressed with every other level when the console-log policy is off. Same channel
// and colour as the catch block below.
if (!isConsoleLogEnabled()) {
cliux.print('CLI_AUTH_WHOAMI_FAILED', { color: 'yellow' });
}
}
} catch (error) {
log.debug('whoami command failed.', { ...this.contextDetails, error });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { configHandler, log, handleAndLogError } from '@contentstack/cli-utilities';
import { configHandler, log, handleAndLogError, cliux, isConsoleLogEnabled } from '@contentstack/cli-utilities';
import { BaseCommand } from '../../../base-command';

export default class ProxyRemoveCommand extends BaseCommand<typeof ProxyRemoveCommand> {
Expand All @@ -17,7 +17,13 @@ export default class ProxyRemoveCommand extends BaseCommand<typeof ProxyRemoveCo

log.debug('Removing proxy configuration from global config', this.contextDetails);
configHandler.delete('proxy');
log.success('Proxy configuration removed from global config successfully', this.contextDetails);
const successMessage = 'Proxy configuration removed from global config successfully';
log.success(successMessage, this.contextDetails);

// Same as config:set:proxy — this is the command's only output at all.
if (!isConsoleLogEnabled()) {
cliux.print(successMessage);
}
} catch (error) {
handleAndLogError(error, { ...this.contextDetails, module: 'config-remove-proxy' });
}
Expand Down
20 changes: 18 additions & 2 deletions packages/contentstack-config/src/commands/config/set/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { flags, configHandler, FlagInput, log, handleAndLogError, cliux } from '@contentstack/cli-utilities';
import {
flags,
configHandler,
FlagInput,
log,
handleAndLogError,
cliux,
isConsoleLogEnabled,
} from '@contentstack/cli-utilities';
import { askProxyPassword } from '../../../utils/interactive';
import { BaseCommand } from '../../../base-command';

Expand Down Expand Up @@ -72,7 +80,15 @@ export default class ProxySetCommand extends BaseCommand<typeof ProxySetCommand>
log.debug('Saving proxy configuration to global config', this.contextDetails);
configHandler.set('proxy', proxyConfig);

log.success('Proxy configuration set successfully', this.contextDetails);
const successMessage = 'Proxy configuration set successfully';
log.success(successMessage, this.contextDetails);

// log.success maps to the info level, which only reaches the console when the
// console-log policy is on. This is the command's only output, so print it directly
// when the policy is off. config has no progress UI — nothing to interleave with.
if (!isConsoleLogEnabled()) {
cliux.print(successMessage);
}
} catch (error) {
handleAndLogError(error, { ...this.contextDetails, module: 'config-set-proxy' });
}
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-utilities/.mocharc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"require": [
"test/helpers/init.js",
"test/helpers/mock-ora.js",
"ts-node/register",
"source-map-support/register",
"test/helpers/mocha-root-hooks.js"
Expand Down
2 changes: 0 additions & 2 deletions packages/contentstack-utilities/src/constants/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,3 @@ export const levelColors = {
info: 'white',
debug: 'blue',
};

export const PROGRESS_SUPPORTED_MODULES = ['export', 'import', 'audit', 'import-setup', 'clone', 'bulk-operations'] as const;
14 changes: 9 additions & 5 deletions packages/contentstack-utilities/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,16 @@ const sensitiveKeys = [
/delivery[-._]?token/i,
];

/**
* @deprecated No-op, kept only so the plugins that still call it keep compiling.
*
* Console visibility is no longer derived from `log.progressSupportedModule` — it is a
* process-wide policy resolved once by the core CLI's `console-policy` init hook (see
* `logger/console-policy.ts`), so there is nothing left to clear. Remove this export a
* release after the plugin call sites are gone.
*/
export function clearProgressModuleSetting(): void {
const logConfig = configHandler.get('log') || {};
if (logConfig?.progressSupportedModule) {
delete logConfig.progressSupportedModule;
configHandler.set('log', logConfig);
}
// Intentionally empty.
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/contentstack-utilities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export type { ChalkInstance } from './chalk';
export { Logger };
export { default as authenticationHandler } from './authentication-handler';
export { v2Logger as log, cliErrorHandler, handleAndLogError, getLogPath, getSessionLogPath } from './logger/log';
// NOTE Only the reader is exported. `setConsoleLogPolicy` stays off the index so that
// nothing downstream of the core CLI's `console-policy` init hook can override the
// decision — the hook imports the setter by deep path.
export { isConsoleLogEnabled } from './logger/console-policy';
export {
CLIProgressManager,
SummaryManager,
Expand Down
5 changes: 5 additions & 0 deletions packages/contentstack-utilities/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export interface ProcessProgress {
}

export interface ProgressManagerOptions {
/**
* Defaults to the process-wide console-log policy (`isConsoleLogEnabled()`), which is
* what production code should rely on. Pass it explicitly only to drive the two modes
* directly, e.g. from tests.
*/
showConsoleLogs?: boolean;
total?: number;
moduleName?: string;
Expand Down
37 changes: 37 additions & 0 deletions packages/contentstack-utilities/src/logger/console-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Console-log policy.
*
* Whether the CLI writes log lines to the console is a single process-wide
* decision, resolved once from static inputs (env var → user config → the
* plugin's `csdxConfig.showConsoleLogs` declaration → `false`) by the
* `console-policy` init hook in the core CLI, before any command code runs.
*
* The default is `false`: the logger writes diagnostics to files and knows
* nothing about the screen. Console output is an opt-in verbosity feature, and
* because the progress UI and the log stream are two consumers of one terminal,
* enabling it also turns the progress UI off (see `CLIProgressManager`).
*
* This module holds no disk state, so it is free to consult per message. The
* decision is deliberately *not* frozen on first read — freezing a value that
* arrives late is the bug this policy replaces.
*
* `setConsoleLogPolicy` is intentionally absent from the package index. The core
* CLI imports it by deep path (`@contentstack/cli-utilities/lib/logger/console-policy`);
* a plugin importing from the index has no setter to call, which makes "nothing
* downstream may override the policy" structural rather than a runtime lock.
*/

let enabled = false;

export function setConsoleLogPolicy(value: boolean): void {
enabled = value;
}

export function isConsoleLogEnabled(): boolean {
return enabled;
}

/** Test-only: restore the default (files-only) policy between cases. */
export function resetConsoleLogPolicy(): void {
enabled = false;
}
67 changes: 27 additions & 40 deletions packages/contentstack-utilities/src/logger/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import traverse from 'traverse';
import { klona } from 'klona/full';
import { normalize } from 'path';
import * as winston from 'winston';
import { levelColors, logLevels, PROGRESS_SUPPORTED_MODULES } from '../constants/logging';
import { levelColors, logLevels } from '../constants/logging';
import { LoggerConfig, LogLevel, LogType } from '../interfaces/index';
import { configHandler } from '..';
import { getSessionLogPath } from './session-path';
import { isConsoleLogEnabled } from './console-policy';

export default class Logger {
private loggers: Record<string, winston.Logger>;
Expand Down Expand Up @@ -69,44 +69,31 @@ export default class Logger {
}),
];

// Determine console logging based on configuration
let showConsoleLogs = true;
if (configHandler && typeof configHandler.get === 'function') {
const logConfig = configHandler.get('log') || {};
const currentModule = logConfig.progressSupportedModule;
const hasProgressSupport = currentModule && PROGRESS_SUPPORTED_MODULES.includes(currentModule);

if (hasProgressSupport) {
// Plugin has progress bars - respect user's explicit setting, or default to false (show progress bars)
showConsoleLogs = logConfig.showConsoleLogs ?? false;
} else {
// Plugin doesn't have progress support - always show console logs
showConsoleLogs = true;
}
}

// Errors and warnings must always reach the console, even when progress bars
// suppress info/success/debug output — otherwise failures (e.g. an invalid
// stack API key or a taxonomy error) are silently swallowed in progress mode.
const isErrorOrWarn = level === 'error' || level === 'warn';

if (showConsoleLogs || isErrorOrWarn) {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf((info) => {
// Apply full redaction for console (user-facing)
const redactedInfo = this.redact(info, true);
const colorizer = winston.format.colorize();
const levelText = redactedInfo.level.toUpperCase();
const { timestamp, message } = redactedInfo;
return colorizer.colorize(redactedInfo.level, `[${timestamp}] ${levelText}: ${message}`);
}),
),
}),
);
}
// The Console transport is always attached; whether it emits is decided per
// message by the console-log policy, never by the transport list. The filter
// below is the FIRST format in the chain so that a `false` return short-circuits
// the write before timestamp/printf run.
//
// It must return `info` (not `{}`) on the enabled path: logform's `combine` feeds
// each format's return value into the next, so returning a fresh object would
// *replace* `info` and the printf below would emit blank lines instead of the
// message. Returning `false` is winston-transport's documented skip signal.
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format((info) => (isConsoleLogEnabled() ? info : false))(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf((info) => {
// Apply full redaction for console (user-facing)
const redactedInfo = this.redact(info, true);
const colorizer = winston.format.colorize();
const levelText = redactedInfo.level.toUpperCase();
const { timestamp, message } = redactedInfo;
return colorizer.colorize(redactedInfo.level, `[${timestamp}] ${levelText}: ${message}`);
}),
),
}),
);

return winston.createLogger({
levels: logLevels,
Expand Down
Loading
Loading