From f97343993f50ca78b9f7d54e56c9af0e9c696047 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Thu, 4 Dec 2025 17:48:29 +0530 Subject: [PATCH 1/4] feat: add session file in session based logger --- .talismanrc | 6 +- .../src/audit-base-command.ts | 20 +- .../contentstack-audit/src/types/context.ts | 5 - .../contentstack-auth/src/base-command.ts | 6 +- .../src/commands/cm/stacks/clone.js | 7 +- .../src/commands/cm/stacks/export.ts | 34 +-- .../contentstack-export/src/types/index.ts | 8 - .../src/commands/cm/stacks/import-setup.ts | 23 +- .../src/types/index.ts | 7 - .../src/commands/cm/stacks/import.ts | 24 +- .../contentstack-import/src/types/index.ts | 22 +- .../contentstack-utilities/src/helpers.ts | 59 +++++ packages/contentstack-utilities/src/index.ts | 1 + .../src/logger/session-path.ts | 73 +++++- .../test/unit/logger.test.ts | 246 ++++++++++++++++++ .../contentstack-variants/src/types/utils.ts | 8 - .../hooks/prerun/latest-version-warning.ts | 6 +- 17 files changed, 433 insertions(+), 122 deletions(-) diff --git a/.talismanrc b/.talismanrc index 630ad8dfb0..a19a142e0e 100644 --- a/.talismanrc +++ b/.talismanrc @@ -212,7 +212,7 @@ fileignoreconfig: - filename: packages/contentstack-export/test/unit/export/modules/personalize.test.ts checksum: 83cf034fabee00b42b4243a8c0b8ba280ab7c1e68ffd741c49c31aaee8ca0315 - filename: packages/contentstack-utilities/test/unit/logger.test.ts - checksum: 11778d0252202c18a1ca6a38883d6e12fc324ff86ad0fe058bc2505f9cd66ba3 + checksum: a1939dea16166b1893a248179524a76f2ed20b04b99c83bd1a5a13fcf6f0dadc - filename: packages/contentstack-audit/test/unit/audit-base-command.test.ts checksum: 17a16b4457c820494442f335d94d0949961e68e8ca72ca0f1fa9d4d0eeb0c17a - filename: packages/contentstack-import/src/import/modules/taxonomies.ts @@ -227,4 +227,8 @@ fileignoreconfig: checksum: 86b11c2a2dd8c0b14aa558e4e52d6d721cd7707422c26a68e96cc5b55b9fefd8 - filename: packages/contentstack-import-setup/src/utils/login-handler.ts checksum: 3860c96e31677356963e67049762f944aef7c7b22fabb75a70ff5c64cf1ac274 +- filename: packages/contentstack-utilities/src/helpers.ts + checksum: 9d7df9d79cec75f238a0072bf79c4934b4724bf1466451ea6f923adfd5c0b75b +- filename: packages/contentstack-utilities/src/logger/session-path.ts + checksum: 4c66980a857bc12012a45e50790c0eaab06883db5e1476d84fb142a08b70b2e7 version: "1.0" diff --git a/packages/contentstack-audit/src/audit-base-command.ts b/packages/contentstack-audit/src/audit-base-command.ts index caeb54f03e..827b0d840f 100644 --- a/packages/contentstack-audit/src/audit-base-command.ts +++ b/packages/contentstack-audit/src/audit-base-command.ts @@ -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, createLogContext } from '@contentstack/cli-utilities'; import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'; import config from './config'; import { print } from './util/log'; @@ -50,18 +50,6 @@ export abstract class AuditBaseCommand extends BaseCommand { this.currentCommand = command; // Initialize audit context - this.auditContext = this.createAuditContext(); + createLogContext(this.context?.info?.command,'', configHandler.get('authenticationMethod')); + this.auditContext = { module: 'audit' }; log.debug(`Starting audit command: ${command}`, this.auditContext); log.info(`Starting audit command: ${command}`, this.auditContext); @@ -224,7 +213,8 @@ export abstract class AuditBaseCommand extends BaseCommand = Interfaces.InferredArgs; @@ -16,7 +16,9 @@ export abstract class BaseCommand extends Command { */ public async init(): Promise { await super.init(); - this.contextDetails = { ...this.createExportContext() }; + // this.contextDetails = { ...this.createExportContext() }; + this.contextDetails = { ...createLogContext(this.context?.info?.command || 'auth', '',) }; + } /** diff --git a/packages/contentstack-clone/src/commands/cm/stacks/clone.js b/packages/contentstack-clone/src/commands/cm/stacks/clone.js index 7f210fc791..b14c681aa4 100644 --- a/packages/contentstack-clone/src/commands/cm/stacks/clone.js +++ b/packages/contentstack-clone/src/commands/cm/stacks/clone.js @@ -73,7 +73,12 @@ class StackCloneCommand extends Command { sourceManagementTokenAlias, destinationManagementTokenAlias, ); - const cloneContext = this.createCloneContext(authenticationMethod); + createLogContext( + this.context?.info?.command || 'cm:stacks:clone', + sourceStackApiKey, + authenticationMethod + ); + cloneContext = { module: 'clone' }; log.debug('Starting clone operation setup', cloneContext); if (externalConfigPath) { diff --git a/packages/contentstack-export/src/commands/cm/stacks/export.ts b/packages/contentstack-export/src/commands/cm/stacks/export.ts index 9a1539ad16..dea0bad91b 100644 --- a/packages/contentstack-export/src/commands/cm/stacks/export.ts +++ b/packages/contentstack-export/src/commands/cm/stacks/export.ts @@ -13,10 +13,11 @@ import { log, handleAndLogError, getLogPath, + createLogContext, } from '@contentstack/cli-utilities'; import { ModuleExporter } from '../../../export'; -import { Context, ExportConfig } from '../../../types'; +import { ExportConfig } from '../../../types'; import { setupExportConfig, writeExportMetaFile } from '../../../utils'; export default class ExportCommand extends Command { @@ -120,9 +121,16 @@ export default class ExportCommand extends Command { try { const { flags } = await this.parse(ExportCommand); const exportConfig = await setupExportConfig(flags); - // Prepare the context object - const context = this.createExportContext(exportConfig.apiKey, exportConfig.authenticationMethod); - exportConfig.context = { ...context }; + + // Store apiKey in configHandler for session.json (return value not needed) + createLogContext( + this.context?.info?.command || 'cm:stacks:export', + exportConfig.apiKey, + exportConfig.authenticationMethod + ); + + // For log entries, only pass module (other fields are in session.json) + exportConfig.context = { module: '' }; //log.info(`Using Cli Version: ${this.context?.cliVersion}`, exportConfig.context); // Assign exportConfig variables @@ -137,29 +145,15 @@ export default class ExportCommand extends Command { } log.success( `The content of the stack ${exportConfig.apiKey} has been exported successfully!`, - exportConfig.context, ); - log.info(`The exported content has been stored at '${exportDir}'`, exportConfig.context); - log.success(`The log has been stored at '${getLogPath()}'`, exportConfig.context); + log.info(`The exported content has been stored at '${exportDir}'`); + log.success(`The log has been stored at '${getLogPath()}'`); } catch (error) { handleAndLogError(error); log.info(`The log has been stored at '${getLogPath()}'`); } } - // Create export context object - private createExportContext(apiKey: string, authenticationMethod?: string): Context { - return { - command: this.context?.info?.command || 'cm:stacks:export', - module: '', - userId: configHandler.get('userUid') || '', - email: configHandler.get('email') || '', - sessionId: this.context?.sessionId || '', - apiKey: apiKey || '', - orgId: configHandler.get('oauthOrgUid') || '', - authenticationMethod: authenticationMethod || 'Basic Auth', - }; - } // Assign values to exportConfig private assignExportConfig(exportConfig: ExportConfig): void { diff --git a/packages/contentstack-export/src/types/index.ts b/packages/contentstack-export/src/types/index.ts index 736b7538dd..bd8dc6a061 100644 --- a/packages/contentstack-export/src/types/index.ts +++ b/packages/contentstack-export/src/types/index.ts @@ -130,15 +130,7 @@ export interface StackConfig { limit?: number; } export interface Context { - command: string; module: string; - userId: string | undefined; - email: string | undefined; - sessionId: string | undefined; - clientId?: string | undefined; - apiKey: string; - orgId: string; - authenticationMethod?: string; } export { default as DefaultConfig } from './default-config'; diff --git a/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts b/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts index 52d0b0c95f..9f02c58192 100644 --- a/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts +++ b/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts @@ -12,6 +12,7 @@ import { log, handleAndLogError, configHandler, + createLogContext, } from '@contentstack/cli-utilities'; import { ImportConfig, Context } from '../../../types'; @@ -71,8 +72,13 @@ export default class ImportSetupCommand extends Command { const { flags } = await this.parse(ImportSetupCommand); let importSetupConfig = await setupImportConfig(flags); // Prepare the context object - const context = this.createImportSetupContext(importSetupConfig.apiKey, (importSetupConfig as any).authenticationMethod); - importSetupConfig.context = { ...context }; + createLogContext( + this.context?.info?.command || 'cm:stacks:import-setup', + importSetupConfig.apiKey, + configHandler.get('authenticationMethod') + ); + + importSetupConfig.context = { module: '' }; // Note setting host to create cma client importSetupConfig.host = this.cmaHost; @@ -94,17 +100,4 @@ export default class ImportSetupCommand extends Command { } } - // Create import setup context object - private createImportSetupContext(apiKey: string, authenticationMethod?: string, module?: string): Context { - return { - command: this.context?.info?.command || 'cm:stacks:import-setup', - module: module || '', - userId: configHandler.get('userUid') || undefined, - email: configHandler.get('email') || undefined, - sessionId: this.context?.sessionId, - apiKey: apiKey || '', - orgId: configHandler.get('oauthOrgUid') || '', - authenticationMethod: authenticationMethod || 'Basic Auth', - }; - } } diff --git a/packages/contentstack-import-setup/src/types/index.ts b/packages/contentstack-import-setup/src/types/index.ts index df9c0b0bda..0c9fc523b7 100644 --- a/packages/contentstack-import-setup/src/types/index.ts +++ b/packages/contentstack-import-setup/src/types/index.ts @@ -154,12 +154,5 @@ export type TaxonomyQueryParams = { }; export interface Context { - command: string; module: string; - userId: string | undefined; - email: string | undefined; - sessionId: string | undefined; - apiKey: string; - orgId: string; - authenticationMethod?: string; } diff --git a/packages/contentstack-import/src/commands/cm/stacks/import.ts b/packages/contentstack-import/src/commands/cm/stacks/import.ts index 00550f9809..16130e84b7 100644 --- a/packages/contentstack-import/src/commands/cm/stacks/import.ts +++ b/packages/contentstack-import/src/commands/cm/stacks/import.ts @@ -11,6 +11,7 @@ import { handleAndLogError, configHandler, getLogPath, + createLogContext, } from '@contentstack/cli-utilities'; import { Context, ImportConfig } from '../../../types'; @@ -155,8 +156,13 @@ export default class ImportCommand extends Command { const { flags } = await this.parse(ImportCommand); importConfig = await setupImportConfig(flags); // Prepare the context object - const context = this.createImportContext(importConfig.apiKey, importConfig.authenticationMethod); - importConfig.context = { ...context }; + createLogContext( + this.context?.info?.command || 'cm:stacks:export', + importConfig.apiKey, + importConfig.authenticationMethod + ); + + importConfig.context = { module: '' }; //log.info(`Using Cli Version: ${this.context?.cliVersion}`, importConfig.context); // Note setting host to create cma client @@ -190,18 +196,4 @@ export default class ImportCommand extends Command { } } } - - // Create export context object - private createImportContext(apiKey: string, authenticationMethod?: string): Context { - return { - command: this.context?.info?.command || 'cm:stacks:import', - module: '', - userId: configHandler.get('userUid') || '', - email: configHandler.get('email') || '', - sessionId: this.context?.sessionId, - apiKey: apiKey || '', - orgId: configHandler.get('oauthOrgUid') || '', - authenticationMethod: authenticationMethod || 'Basic Auth', - }; - } } diff --git a/packages/contentstack-import/src/types/index.ts b/packages/contentstack-import/src/types/index.ts index 88f3084c90..be5403cd9c 100644 --- a/packages/contentstack-import/src/types/index.ts +++ b/packages/contentstack-import/src/types/index.ts @@ -106,15 +106,7 @@ export interface TaxonomiesConfig { } export interface Context { - command: string; module: string; - userId: string | undefined; - email: string | undefined; - sessionId: string | undefined; - clientId?: string | undefined; - apiKey: string; - orgId: string; - authenticationMethod?: string; } export { default as DefaultConfig } from './default-config'; @@ -127,16 +119,4 @@ export type ExtensionType = { uid: string; scope: Record; title: string; -}; - -export interface Context { - command: string; - module: string; - userId: string | undefined; - email: string | undefined; - sessionId: string | undefined; - clientId?: string | undefined; - apiKey: string; - orgId: string; - authenticationMethod?: string; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/contentstack-utilities/src/helpers.ts b/packages/contentstack-utilities/src/helpers.ts index 7156f99935..316a6a4138 100644 --- a/packages/contentstack-utilities/src/helpers.ts +++ b/packages/contentstack-utilities/src/helpers.ts @@ -245,3 +245,62 @@ const sensitiveKeys = [ /management[-._]?token/i, /delivery[-._]?token/i, ]; + +/** + * Get authentication method from config + * @returns Authentication method string ('OAuth', 'Basic Auth', or empty string) + */ +export function getAuthenticationMethod(): string { + const authType = configHandler.get('authorisationType'); + if (authType === 'OAUTH') { + return 'OAuth'; + } else if (authType === 'BASIC') { + return 'Basic Auth'; + } + // Management token detection is command-specific and not stored globally + // Return empty string if unknown + return ''; +} + +/** + * Creates a standardized context object for logging + * This context contains all session-level metadata that should be in session.json + * The apiKey is stored in configHandler so it's available for session.json generation + * + * @param commandId - The command ID (e.g., 'cm:stacks:export') + * @param apiKey - The API key for the stack (will be stored in configHandler for session.json) + * @param authenticationMethod - Optional authentication method + * @returns Context object with all session-level metadata + */ +export function createLogContext( + commandId: string, + apiKey: string, + authenticationMethod?: string +): { + command: string; + module: string; + userId: string; + email: string; + sessionId: string; + apiKey: string; + orgId: string; + authenticationMethod: string; +} { + // Store apiKey in configHandler so it's available for session.json + if (apiKey) { + configHandler.set('apiKey', apiKey); + } + + const authMethod = authenticationMethod || getAuthenticationMethod(); + + return { + command: commandId, + module: '', + userId: configHandler.get('clientId') || '', + email: configHandler.get('email') || '', + sessionId: configHandler.get('sessionId') || '', + apiKey: apiKey || '', + orgId: configHandler.get('oauthOrgUid') || '', + authenticationMethod: authMethod, + }; +} diff --git a/packages/contentstack-utilities/src/index.ts b/packages/contentstack-utilities/src/index.ts index be28362868..daca328264 100644 --- a/packages/contentstack-utilities/src/index.ts +++ b/packages/contentstack-utilities/src/index.ts @@ -27,6 +27,7 @@ export * from './fs-utility'; export { default as NodeCrypto } from './encrypter'; export { Args as args, Flags as flags, Command } from './cli-ux'; export * from './helpers'; +export { createLogContext } from './helpers'; export * from './interfaces'; export * from './date-time'; export * from './add-locale'; diff --git a/packages/contentstack-utilities/src/logger/session-path.ts b/packages/contentstack-utilities/src/logger/session-path.ts index 2d5de07efd..89aaa7f68b 100644 --- a/packages/contentstack-utilities/src/logger/session-path.ts +++ b/packages/contentstack-utilities/src/logger/session-path.ts @@ -1,8 +1,65 @@ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; -import { configHandler, formatDate, formatTime } from '..'; +import { configHandler, formatDate, formatTime, createLogContext } from '..'; import { getLogPath } from './log'; +/** + * Extract module name from command ID + * Example: "cm:stacks:audit" -> "audit" + */ +function extractModule(commandId: string): string { + if (!commandId || commandId === 'unknown') { + return ''; + } + // Split by colon and get the last part + const parts = commandId.split(':'); + return parts[parts.length - 1] || ''; +} + +/** + * Generate session metadata object for session.json + * Uses createLogContext() to get base context, then adds session-specific metadata + */ +function generateSessionMetadata( + commandId: string, + sessionId: string, + startTimestamp: Date, +): Record { + const originalCommandId = configHandler.get('currentCommandId') || commandId; + const module = extractModule(originalCommandId); + const apiKey = configHandler.get('apiKey') || ''; + + const baseContext = createLogContext(originalCommandId, apiKey); + + return { + ...baseContext, + module: module, + sessionId: sessionId, + startTimestamp: startTimestamp.toISOString(), + MachineEnvironment: { + nodeVersion: process.version, + os: os.platform(), + hostname: os.hostname(), + CLI_VERSION: configHandler.get('CLI_VERSION') || '', + }, + }; +} + +/** + * Create session.json metadata file in the session directory + */ +function createSessionMetadataFile(sessionPath: string, metadata: Record): void { + const metadataPath = path.join(sessionPath, 'session.json'); + try { + fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8'); + } catch (error) { + // Silently fail if metadata file cannot be created + // Logging here would cause circular dependency + // The session folder and logs will still be created + } +} + /** * Get the session-based log path for date-organized logging * Structure: {basePath}/{YYYY-MM-DD}/{command}-{YYYYMMDD-HHMMSS}-{sessionId}/ @@ -42,10 +99,22 @@ export function getSessionLogPath(): string { const sessionPath = path.join(basePath, dateStr, sessionFolderName); // Ensure directory exists - if (!fs.existsSync(sessionPath)) { + const isNewSession = !fs.existsSync(sessionPath); + if (isNewSession) { fs.mkdirSync(sessionPath, { recursive: true }); } + // Create session.json metadata file for new sessions + // This ensures metadata is created before any logs are written + if (isNewSession) { + const metadata = generateSessionMetadata( + configHandler.get('currentCommandId') || commandId, + sessionId, + now, + ); + createSessionMetadataFile(sessionPath, metadata); + } + return sessionPath; } diff --git a/packages/contentstack-utilities/test/unit/logger.test.ts b/packages/contentstack-utilities/test/unit/logger.test.ts index 502d9d5d94..997c4fcf46 100644 --- a/packages/contentstack-utilities/test/unit/logger.test.ts +++ b/packages/contentstack-utilities/test/unit/logger.test.ts @@ -450,5 +450,251 @@ describe('Session Log Path', () => { const dateFolder = path.dirname(logDir); expect(fs.existsSync(dateFolder)).to.be.true; expect(dateFolder).to.match(/\d{4}-\d{2}-\d{2}/); + + // Verify the log file exists (winston writes asynchronously, so wait a bit) + // Wait for winston to flush the file with timeout + return new Promise((resolve, reject) => { + const maxAttempts = 20; // 20 * 50ms = 1 second max wait + let attempts = 0; + const checkFile = () => { + attempts++; + if (fs.existsSync(actualLogFile)) { + resolve(); + } else if (attempts >= maxAttempts) { + reject(new Error(`Log file ${actualLogFile} was not created within timeout`)); + } else { + setTimeout(checkFile, 50); + } + }; + checkFile(); + }).then(() => { + expect(fs.existsSync(actualLogFile)).to.be.true; + }); + }); + + describe('Session Metadata (session.json)', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = path.join(os.tmpdir(), `cli-test-${Date.now()}-${Math.random().toString(36).substring(7)}`); + configHandler.delete('currentCommandId'); + configHandler.delete('sessionId'); + configHandler.delete('email'); + configHandler.delete('authorisationType'); + }); + + afterEach(() => { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + configHandler.delete('currentCommandId'); + configHandler.delete('sessionId'); + configHandler.delete('email'); + configHandler.delete('authorisationType'); + }); + + fancy + .stub(configHandler, 'get', (...args: any[]) => { + const key = args[0]; + if (key === 'log.path') return tempDir; + if (key === 'currentCommandId') return 'cm:stacks:audit'; + if (key === 'sessionId') return 'a3f8c9'; + if (key === 'email') return 'user@example.com'; + if (key === 'authorisationType') return 'OAUTH'; + return undefined; + }) + .it('should create session.json with correct metadata structure', () => { + const sessionPath = getSessionLogPath(); + const metadataPath = path.join(sessionPath, 'session.json'); + + // Verify session.json exists + expect(fs.existsSync(metadataPath)).to.be.true; + + // Read and parse session.json + const metadataContent = fs.readFileSync(metadataPath, 'utf8'); + const metadata = JSON.parse(metadataContent); + + // Verify required fields + expect(metadata).to.have.property('command'); + expect(metadata).to.have.property('module'); + expect(metadata).to.have.property('sessionId'); + expect(metadata).to.have.property('startTimestamp'); + expect(metadata).to.have.property('authenticationMethod'); + expect(metadata).to.have.property('email'); + expect(metadata).to.have.property('MachineEnvironment'); + + // Verify values + expect(metadata.command).to.equal('cm:stacks:audit'); + expect(metadata.module).to.equal('audit'); + expect(metadata.sessionId).to.equal('a3f8c9'); + expect(metadata.authenticationMethod).to.equal('OAuth'); + expect(metadata.email).to.equal('user@example.com'); + + // Verify MachineEnvironment object + expect(metadata.MachineEnvironment).to.have.property('nodeVersion'); + expect(metadata.MachineEnvironment).to.have.property('os'); + expect(metadata.MachineEnvironment).to.have.property('hostname'); + expect(metadata.MachineEnvironment.nodeVersion).to.equal(process.version); + expect(metadata.MachineEnvironment.os).to.equal(process.platform); + expect(metadata.MachineEnvironment.hostname).to.equal(os.hostname()); + + // Verify timestamp is ISO format + expect(metadata.startTimestamp).to.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + }); + + fancy + .stub(configHandler, 'get', (...args: any[]) => { + const key = args[0]; + if (key === 'log.path') return tempDir; + if (key === 'currentCommandId') return 'cm:stacks:export'; + if (key === 'sessionId') return 'test-session-123'; + if (key === 'email') return undefined; + if (key === 'authorisationType') return 'BASIC'; + return undefined; + }) + .it('should create session.json with Basic Auth authentication method', () => { + const sessionPath = getSessionLogPath(); + const metadataPath = path.join(sessionPath, 'session.json'); + + const metadataContent = fs.readFileSync(metadataPath, 'utf8'); + const metadata = JSON.parse(metadataContent); + + expect(metadata.authenticationMethod).to.equal('Basic Auth'); + expect(metadata.email).to.equal(''); + expect(metadata.module).to.equal('export'); + }); + + fancy + .stub(configHandler, 'get', (...args: any[]) => { + const key = args[0]; + if (key === 'log.path') return tempDir; + if (key === 'currentCommandId') return 'cm:stacks:import'; + if (key === 'sessionId') return 'test-session-456'; + if (key === 'email') return 'test@example.com'; + if (key === 'authorisationType') return undefined; + return undefined; + }) + .it('should create session.json with empty authentication method when not set', () => { + const sessionPath = getSessionLogPath(); + const metadataPath = path.join(sessionPath, 'session.json'); + + const metadataContent = fs.readFileSync(metadataPath, 'utf8'); + const metadata = JSON.parse(metadataContent); + + expect(metadata.authenticationMethod).to.equal(''); + expect(metadata.email).to.equal('test@example.com'); + expect(metadata.module).to.equal('import'); + }); + + fancy + .stub(configHandler, 'get', (...args: any[]) => { + const key = args[0]; + if (key === 'log.path') return tempDir; + if (key === 'currentCommandId') return 'unknown'; + if (key === 'sessionId') return 'test-session-789'; + if (key === 'email') return undefined; + if (key === 'authorisationType') return undefined; + return undefined; + }) + .it('should create session.json with empty module when command is unknown', () => { + const sessionPath = getSessionLogPath(); + const metadataPath = path.join(sessionPath, 'session.json'); + + const metadataContent = fs.readFileSync(metadataPath, 'utf8'); + const metadata = JSON.parse(metadataContent); + + expect(metadata.command).to.equal('unknown'); + expect(metadata.module).to.equal(''); + }); + + fancy + .stub(configHandler, 'get', (...args: any[]) => { + const key = args[0]; + if (key === 'log.path') return tempDir; + if (key === 'currentCommandId') return 'cm:stacks:audit'; + if (key === 'sessionId') return 'a3f8c9'; + if (key === 'email') return 'user@example.com'; + if (key === 'authorisationType') return 'OAUTH'; + return undefined; + }) + .it('should create session.json only once per session folder', () => { + // First call creates the session folder and metadata + const sessionPath1 = getSessionLogPath(); + const metadataPath = path.join(sessionPath1, 'session.json'); + + // Read first metadata + const metadataContent1 = fs.readFileSync(metadataPath, 'utf8'); + const metadata1 = JSON.parse(metadataContent1); + const firstTimestamp = metadata1.startTimestamp; + + // Second call should return same path (session already exists) + const sessionPath2 = getSessionLogPath(); + expect(sessionPath1).to.equal(sessionPath2); + + // Verify metadata file still exists and wasn't overwritten + expect(fs.existsSync(metadataPath)).to.be.true; + const metadataContent2 = fs.readFileSync(metadataPath, 'utf8'); + const metadata2 = JSON.parse(metadataContent2); + + // Timestamp should be the same (created only once) + expect(metadata2.startTimestamp).to.equal(firstTimestamp); + }); + + fancy + .stub(configHandler, 'get', (...args: any[]) => { + const key = args[0]; + if (key === 'log.path') return tempDir; + if (key === 'currentCommandId') return 'cm:stacks:clone'; + if (key === 'sessionId') return 'clone-session'; + if (key === 'email') return 'clone@example.com'; + if (key === 'authorisationType') return 'BASIC'; + return undefined; + }) + .it('should create session.json before any logs are written', () => { + const sessionPath = getSessionLogPath(); + const metadataPath = path.join(sessionPath, 'session.json'); + + // Verify session.json exists immediately after getSessionLogPath + expect(fs.existsSync(metadataPath)).to.be.true; + + // Now create logger and write a log + const logger = new Logger({ + basePath: tempDir, + consoleLogLevel: 'info', + logLevel: 'info', + }); + + const winLogger = logger.getLoggerInstance('error'); + winLogger.error('Test log entry'); + + // Verify session.json still exists and wasn't overwritten + expect(fs.existsSync(metadataPath)).to.be.true; + + // Verify log file exists (winston writes asynchronously, so wait a bit) + const logFilePath = path.join(sessionPath, 'error.log'); + return new Promise((resolve, reject) => { + const maxAttempts = 20; // 20 * 50ms = 1 second max wait + let attempts = 0; + const checkFile = () => { + attempts++; + if (fs.existsSync(logFilePath)) { + resolve(); + } else if (attempts >= maxAttempts) { + reject(new Error(`Log file ${logFilePath} was not created within timeout`)); + } else { + setTimeout(checkFile, 50); + } + }; + checkFile(); + }).then(() => { + expect(fs.existsSync(logFilePath)).to.be.true; + + // Verify metadata is valid JSON + const metadataContent = fs.readFileSync(metadataPath, 'utf8'); + const metadata = JSON.parse(metadataContent); + expect(metadata.command).to.equal('cm:stacks:clone'); + expect(metadata.module).to.equal('clone'); + }); + }); }); }); diff --git a/packages/contentstack-variants/src/types/utils.ts b/packages/contentstack-variants/src/types/utils.ts index 888b835d7f..1faf91eed0 100644 --- a/packages/contentstack-variants/src/types/utils.ts +++ b/packages/contentstack-variants/src/types/utils.ts @@ -8,13 +8,5 @@ export interface LogType { } export interface Context { - command: string; module: string; - userId: string | undefined; - email: string | undefined; - sessionId: string | undefined; - clientId?: string | undefined; - apiKey: string; - orgId: string; - authMethod?: string; } \ No newline at end of file diff --git a/packages/contentstack/src/hooks/prerun/latest-version-warning.ts b/packages/contentstack/src/hooks/prerun/latest-version-warning.ts index 9136b5c370..15bb90a0c7 100644 --- a/packages/contentstack/src/hooks/prerun/latest-version-warning.ts +++ b/packages/contentstack/src/hooks/prerun/latest-version-warning.ts @@ -3,7 +3,7 @@ import * as semver from 'semver'; import { IVersionUpgradeCache, IVersionUpgradeWarningFrequency } from '../../interfaces'; const versionUpgradeWarningFrequency: IVersionUpgradeWarningFrequency = { - versionSyncDuration: 3 * 24 * 60 * 60 * 1000, + versionSyncDuration: 3 * 24 * 60 * 60 * 1000, // 3 days }; export default async function (_opts): Promise { const now = Date.now(); @@ -11,6 +11,10 @@ export default async function (_opts): Promise { const logger: LoggerService = new LoggerService(process.env.CS_CLI_LOG_PATH || process.cwd(), 'cli-log'); let cache: IVersionUpgradeCache = { lastChecked: 0, lastWarnedDate: '', latestVersion: '' }; + if(!configHandler.get('CLI_VERSION') || configHandler.get('CLI_VERSION') !== this.config.version) { // if CLI_VERSION is not set or is not the same as the current version, set it + configHandler.set('CLI_VERSION', this.config.version); // set current version in configHandler + } + if (!configHandler.get('versionUpgradeWarningFrequency')) { configHandler.set('versionUpgradeWarningFrequency', versionUpgradeWarningFrequency); } From 7ecdfac2ec96a3e24b4b44506c16d7dd6f54abce Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Thu, 4 Dec 2025 17:56:19 +0530 Subject: [PATCH 2/4] updated import test cases --- .talismanrc | 2 +- .../unit/commands/cm/stacks/import.test.ts | 62 ++++++++++++------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/.talismanrc b/.talismanrc index a19a142e0e..311ede6bec 100644 --- a/.talismanrc +++ b/.talismanrc @@ -110,7 +110,7 @@ fileignoreconfig: - filename: packages/contentstack-audit/src/modules/content-types.ts checksum: ddf7b08e6a80af09c6a7019a637c26089fb76572c7c3d079a8af244b02985f16 - filename: packages/contentstack-import/test/unit/commands/cm/stacks/import.test.ts - checksum: b11e57f1b824d405f86438e9e7c59183f8c59b66b42d8d16dbeaf76195a30548 + checksum: ead3c34bad34f912d8663599273d26a95bb48220e16d9e9e3d33f5c064a487a5 - filename: packages/contentstack-import/test/unit/utils/asset-helper.test.ts checksum: 8e83200ac8028f9289ff1bd3a50d191b35c8e28f1854141c90fa1b0134d6bf8a - filename: packages/contentstack-import/test/unit/import/modules/marketplace-apps.test.ts diff --git a/packages/contentstack-import/test/unit/commands/cm/stacks/import.test.ts b/packages/contentstack-import/test/unit/commands/cm/stacks/import.test.ts index fc0829a963..4782a70249 100644 --- a/packages/contentstack-import/test/unit/commands/cm/stacks/import.test.ts +++ b/packages/contentstack-import/test/unit/commands/cm/stacks/import.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { fancy } from 'fancy-test'; import sinon from 'sinon'; -import { managementSDKClient, configHandler, log, handleAndLogError, getLogPath } from '@contentstack/cli-utilities'; +import { managementSDKClient, configHandler, log, handleAndLogError, getLogPath, createLogContext } from '@contentstack/cli-utilities'; import ImportCommand from '../../../../../src/commands/cm/stacks/import'; import { ModuleImporter } from '../../../../../src/import'; import { ImportConfig } from '../../../../../src/types'; @@ -201,18 +201,27 @@ describe('ImportCommand', () => { }); }); - describe('createImportContext', () => { + describe('createLogContext', () => { let configHandlerStub: sinon.SinonStub; + let configHandlerSetStub: sinon.SinonStub; beforeEach(() => { configHandlerStub = sinon.stub(configHandler, 'get'); - configHandlerStub.withArgs('userUid').returns('user-123'); + configHandlerSetStub = sinon.stub(configHandler, 'set'); + configHandlerStub.withArgs('clientId').returns('user-123'); configHandlerStub.withArgs('email').returns('test@example.com'); + configHandlerStub.withArgs('sessionId').returns('test-session-123'); configHandlerStub.withArgs('oauthOrgUid').returns('org-123'); + configHandlerStub.withArgs('authorisationType').returns('BASIC'); + }); + + afterEach(() => { + configHandlerStub.restore(); + configHandlerSetStub.restore(); }); it('should create context with all required properties', () => { - const context = command['createImportContext']('test', 'Basic Auth'); + const context = createLogContext('cm:stacks:import', 'test', 'Basic Auth'); expect(context).to.have.property('command', 'cm:stacks:import'); expect(context).to.have.property('module', ''); @@ -222,10 +231,11 @@ describe('ImportCommand', () => { expect(context).to.have.property('apiKey', 'test'); expect(context).to.have.property('orgId', 'org-123'); expect(context).to.have.property('authenticationMethod', 'Basic Auth'); + expect(configHandlerSetStub.calledWith('apiKey', 'test')).to.be.true; }); it('should use default authentication method when not provided', () => { - const context = command['createImportContext']('test'); + const context = createLogContext('cm:stacks:import', 'test'); expect(context.authenticationMethod).to.equal('Basic Auth'); }); @@ -234,7 +244,7 @@ describe('ImportCommand', () => { configHandlerStub.reset(); configHandlerStub.returns(undefined); - const context = command['createImportContext']('test', 'Management Token'); + const context = createLogContext('cm:stacks:import', 'test', 'Management Token'); expect(context.userId).to.equal(''); expect(context.email).to.equal(''); @@ -242,14 +252,14 @@ describe('ImportCommand', () => { expect(context.authenticationMethod).to.equal('Management Token'); }); - it('should use context command when available', () => { - const context = command['createImportContext']('test'); + it('should use provided command', () => { + const context = createLogContext('cm:stacks:import', 'test'); expect(context.command).to.equal('cm:stacks:import'); }); it('should handle empty apiKey', () => { - const context = command['createImportContext'](''); + const context = createLogContext('cm:stacks:import', ''); expect(context.apiKey).to.equal(''); }); @@ -503,35 +513,43 @@ describe('ImportCommand', () => { configHandlerStub = sinon.stub(configHandler, 'get'); }); - it('should handle undefined context', () => { - (command as any).context = undefined; + afterEach(() => { + configHandlerStub.restore(); + }); + + it('should handle command string directly', () => { + configHandlerStub.returns(undefined); - const context = command['createImportContext']('test'); + const context = createLogContext('cm:stacks:import', 'test'); expect(context.command).to.equal('cm:stacks:import'); }); - it('should handle context without info', () => { - (command as any).context = { sessionId: 'test-session' }; + it('should use command string when provided', () => { + configHandlerStub.withArgs('clientId').returns('user-123'); + configHandlerStub.withArgs('email').returns('test@example.com'); + configHandlerStub.withArgs('sessionId').returns('test-session'); + configHandlerStub.withArgs('oauthOrgUid').returns('org-123'); + configHandlerStub.withArgs('authorisationType').returns('BASIC'); - const context = command['createImportContext']('test'); + const context = createLogContext('cm:stacks:import', 'test'); expect(context.command).to.equal('cm:stacks:import'); + expect(context.sessionId).to.equal('test-session'); }); - it('should handle context without sessionId', () => { - (command as any).context = { info: { command: 'test' } }; + it('should handle missing sessionId from configHandler', () => { + configHandlerStub.returns(undefined); - const context = command['createImportContext']('test'); + const context = createLogContext('cm:stacks:import', 'test'); - expect(context.sessionId).to.be.undefined; + expect(context.sessionId).to.equal(''); }); - it('should handle configHandler throwing errors', () => { - configHandlerStub.reset(); + it('should handle configHandler returning undefined values', () => { configHandlerStub.returns(undefined); - const context = command['createImportContext']('test'); + const context = createLogContext('cm:stacks:import', 'test'); expect(context.userId).to.equal(''); expect(context.email).to.equal(''); From c863bd72d6fd11a81356a35f83fab2cc5651d4a8 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Mon, 8 Dec 2025 20:21:13 +0530 Subject: [PATCH 3/4] resolved comments --- packages/contentstack-audit/src/audit-base-command.ts | 5 ++--- .../contentstack/src/hooks/prerun/latest-version-warning.ts | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contentstack-audit/src/audit-base-command.ts b/packages/contentstack-audit/src/audit-base-command.ts index 827b0d840f..d16236c213 100644 --- a/packages/contentstack-audit/src/audit-base-command.ts +++ b/packages/contentstack-audit/src/audit-base-command.ts @@ -59,8 +59,8 @@ export abstract class AuditBaseCommand extends BaseCommand { this.currentCommand = command; - // Initialize audit context - createLogContext(this.context?.info?.command,'', configHandler.get('authenticationMethod')); + // Initialize audit context (reused, no need to call again in scanAndFix) + createLogContext(this.context?.info?.command, '', configHandler.get('authenticationMethod')); this.auditContext = { module: 'audit' }; log.debug(`Starting audit command: ${command}`, this.auditContext); log.info(`Starting audit command: ${command}`, this.auditContext); @@ -213,7 +213,6 @@ export abstract class AuditBaseCommand extends BaseCommand { const logger: LoggerService = new LoggerService(process.env.CS_CLI_LOG_PATH || process.cwd(), 'cli-log'); let cache: IVersionUpgradeCache = { lastChecked: 0, lastWarnedDate: '', latestVersion: '' }; - if(!configHandler.get('CLI_VERSION') || configHandler.get('CLI_VERSION') !== this.config.version) { // if CLI_VERSION is not set or is not the same as the current version, set it + // if CLI_VERSION is not set or is not the same as the current version, set it + if (!configHandler.get('CLI_VERSION') || configHandler.get('CLI_VERSION') !== this.config.version) { configHandler.set('CLI_VERSION', this.config.version); // set current version in configHandler } From ada8f3fd6b07917facc375431468718dd12a5163 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Mon, 8 Dec 2025 20:28:15 +0530 Subject: [PATCH 4/4] fix test cases --- .../test/unit/export/modules/assets.test.ts | 6 ++++++ .../test/unit/export/modules/base-class.test.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/contentstack-export/test/unit/export/modules/assets.test.ts b/packages/contentstack-export/test/unit/export/modules/assets.test.ts index d865cd4c13..56ef04ef72 100644 --- a/packages/contentstack-export/test/unit/export/modules/assets.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/assets.test.ts @@ -213,6 +213,12 @@ describe('ExportAssets', () => { dirName: 'attributes', fileName: 'attributes.json', invalidKeys: [] + }, + 'composable-studio': { + dirName: 'composable_studio', + fileName: 'composable_studio.json', + apiBaseUrl: 'https://api.contentstack.io', + apiVersion: 'v3' } } } as ExportConfig; diff --git a/packages/contentstack-export/test/unit/export/modules/base-class.test.ts b/packages/contentstack-export/test/unit/export/modules/base-class.test.ts index 426ffe8292..0ffe4187f1 100644 --- a/packages/contentstack-export/test/unit/export/modules/base-class.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/base-class.test.ts @@ -231,6 +231,12 @@ describe('BaseClass', () => { dirName: 'attributes', fileName: 'attributes.json', invalidKeys: [] + }, + 'composable-studio': { + dirName: 'composable_studio', + fileName: 'composable_studio.json', + apiBaseUrl: 'https://api.contentstack.io', + apiVersion: 'v3' } } } as ExportConfig;