From 56bc1ae6202f2fa2c1fa735e10ca6d9ab872be47 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Thu, 23 Jul 2026 11:54:39 +0530 Subject: [PATCH 1/2] fix(export): detect AM 2.0 via plan-check, skip assets under management token (DX-9314) Exporting an AM 2.0 (spaces-based) stack with a management token silently produced the legacy asset layout, and the subsequent import reported success while entry-to-asset references were broken. Management tokens are rejected by the CS Assets APIs, and the linked-workspaces lookup returns [] on any failure, so AM 2.0 went undetected under --alias and the export emitted the wrong layout with no signal. Detect AM 2.0 up front via the feature-status plan-check (which accepts a management token) into config.planStatus. When the org is AM 2.0 and auth is a management token, skip the AM 2.0 asset export with a loud warning instead of emitting the legacy layout; the skip is surfaced in the final summary and logged to error.log. Export-only; import-side fix and a first-class skipped summary status are tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- .talismanrc | 2 + .../src/commands/cm/stacks/export.ts | 2 +- .../src/export/modules/assets.ts | 107 +++++++++++++++--- .../src/types/export-config.ts | 2 + .../src/utils/export-config-handler.ts | 41 ++++++- 5 files changed, 135 insertions(+), 19 deletions(-) diff --git a/.talismanrc b/.talismanrc index 126602cf1..62b854ab8 100644 --- a/.talismanrc +++ b/.talismanrc @@ -15,4 +15,6 @@ fileignoreconfig: checksum: 35451ca4c03359b06531a8461091f4a3a45c4dea1f8853081fb53e4ce28c1cc3 - filename: packages/contentstack-bulk-operations/test/unit/base-bulk-command.test.ts checksum: ca699a9d73757d44ded4d9d27beb4f460f911a7c88a14551a9402a3ac0c69873 +- filename: packages/contentstack-export/src/utils/export-config-handler.ts + checksum: 7e9807759211ce97c041462fa12fd77e76e6003d5ff5774d2107d5ae9cec1705 version: "" diff --git a/packages/contentstack-export/src/commands/cm/stacks/export.ts b/packages/contentstack-export/src/commands/cm/stacks/export.ts index 8a2fc359a..5a769cfad 100644 --- a/packages/contentstack-export/src/commands/cm/stacks/export.ts +++ b/packages/contentstack-export/src/commands/cm/stacks/export.ts @@ -111,7 +111,7 @@ export default class ExportCommand extends Command { let exportDir: string = pathValidator('logs'); try { const { flags } = await this.parse(ExportCommand); - const exportConfig = await setupExportConfig(flags); + const exportConfig = await setupExportConfig(flags, this.context); // Prepare the context object const context = this.createExportContext(exportConfig.apiKey, exportConfig.authenticationMethod); exportConfig.context = { ...context }; diff --git a/packages/contentstack-export/src/export/modules/assets.ts b/packages/contentstack-export/src/export/modules/assets.ts index 6121dbd6d..315c3a011 100644 --- a/packages/contentstack-export/src/export/modules/assets.ts +++ b/packages/contentstack-export/src/export/modules/assets.ts @@ -20,6 +20,7 @@ import { handleAndLogError, messageHandler, CLIProgressManager, + FEATURE, } from '@contentstack/cli-utilities'; import { PATH_CONSTANTS } from '../../constants'; @@ -37,6 +38,23 @@ import { } from '../../utils'; import { handle } from '@oclif/core'; +// cli-utilities' CLIProgressManager.globalSummary is runtime-accessible but typed private. These +// shapes describe the slice we mutate; the single accessor (getGlobalSummary) owns the structural +// cast + feature-detect so applyAssetSummaryCounts and markAssetsSkippedInSummary don't each copy it. +type SummaryModuleShape = { + status: string; + successCount: number; + failureCount: number; + failures: Array<{ item: string; error: string }>; + endTime?: number; +}; +type GlobalSummaryShape = { + getModules(): Map; + registerModule(name: string, totalItems?: number): void; + startModule(name: string): void; + completeModule(name: string, success?: boolean): void; +}; + export default class ExportAssets extends BaseClass { private assetsRootPath: string; public assetConfig = config.modules.assets; @@ -58,23 +76,13 @@ export default class ExportAssets extends BaseClass { } /** - * Bug 3 — push real CS Assets entity counts into the final EXPORT summary: override the ASSETS - * module to count downloaded binaries only, and add dedicated ASSET TYPES / FIELDS / FOLDERS rows. - * Drives the global summary directly (no cli-utilities change); the live multibar is unaffected. - * The ASSETS strategy is set to Default so applyStrategyCorrections does not overwrite these. + * cli-utilities' CLIProgressManager.globalSummary is runtime-accessible but typed private. Reach it + * via a single structural cast + feature-detect here so both callers share one copy of the + * private-shape assumption; returns null (caller degrades) if a future cli-utilities version + * changes the shape. */ - private applyAssetSummaryCounts(counts: AssetExportCounts): void { - type SummaryModule = { successCount: number; failureCount: number }; - type GlobalSummary = { - getModules(): Map; - registerModule(name: string, totalItems?: number): void; - startModule(name: string): void; - completeModule(name: string, success?: boolean): void; - }; - // globalSummary is runtime-accessible but typed private; reach it via a structural cast. - const gs = (CLIProgressManager as unknown as { globalSummary?: GlobalSummary | null }).globalSummary; - // We reach into cli-utilities' summary internals, so feature-detect the shape and DEGRADE - // (skip the count overrides) instead of throwing if a future version changes it. + private getGlobalSummary(): GlobalSummaryShape | null { + const gs = (CLIProgressManager as unknown as { globalSummary?: GlobalSummaryShape | null }).globalSummary; if ( !gs || typeof gs.getModules !== 'function' || @@ -82,6 +90,20 @@ export default class ExportAssets extends BaseClass { typeof gs.startModule !== 'function' || typeof gs.completeModule !== 'function' ) { + return null; + } + return gs; + } + + /** + * Bug 3 — push real CS Assets entity counts into the final EXPORT summary: override the ASSETS + * module to count downloaded binaries only, and add dedicated ASSET TYPES / FIELDS / FOLDERS rows. + * Drives the global summary directly (no cli-utilities change); the live multibar is unaffected. + * The ASSETS strategy is set to Default so applyStrategyCorrections does not overwrite these. + */ + private applyAssetSummaryCounts(counts: AssetExportCounts): void { + const gs = this.getGlobalSummary(); + if (!gs) { log.debug('Global summary shape unavailable; skipping CS Assets summary count overrides', this.exportConfig.context); return; } @@ -114,7 +136,60 @@ export default class ExportAssets extends BaseClass { } } + /** + * DX-9314 — record the AM 2.0 asset skip in the FINAL export summary without a live progress bar. + * We do NOT call createNestedProgress (it would print an empty "ASSETS:" section); instead we reach + * the global summary directly (same structural cast + feature-detect + degrade pattern as + * applyAssetSummaryCounts) and push a failure entry so the Module Details row shows `✗ ASSETS` and the + * Failure Summary prints the reason verbatim. failureCount stays 0 so the row reads `0/0 items`. + * NOTE: this reuses the failure channel deliberately — a first-class "skipped" summary status is a + * separate cli-utilities ticket. + */ + private markAssetsSkippedInSummary(reason: string): void { + const gs = this.getGlobalSummary(); + if (!gs) { + log.debug('Global summary shape unavailable; skipping ASSETS skip row', this.exportConfig.context); + return; + } + + try { + const name = this.currentModuleName.toUpperCase(); + gs.registerModule(name); + gs.startModule(name); + const module = gs.getModules().get(name); + if (module) { + module.failures.push({ item: reason, error: reason }); + module.status = 'failed'; + module.endTime = Date.now(); + } + } catch (e) { + log.debug(`Failed to mark ASSETS as skipped in summary: ${e}`, this.exportConfig.context); + } + } + async start(): Promise { + // AM 2.0 assets cannot be exported with a management token — the CS Assets API only + // accepts a logged-in session (auth token / OAuth). Today the linked-workspaces lookup silently + // returns [] under a management token, so the export falls back to the legacy layout and import + // later reports success with broken entry->asset references. Detect the AM 2.0 org up front via + // the plan-check (which DOES accept a management token) and skip the assets module with a loud + // warning instead of silently emitting the wrong layout. + const amInPlan = this.exportConfig.planStatus?.[FEATURE.ASSET_MANAGEMENT]?.is_part_of_plan; + if (amInPlan && this.exportConfig.management_token) { + const warning = + 'Skipping AM 2.0 asset export: management token authentication is not supported by the Assets APIs. ' + + 'Entry-to-asset references will NOT resolve in the exported content. ' + + 'Re-run the export with a logged-in session (auth token or OAuth) to export AM 2.0 assets.'; + cliux.print(`\nWARNING!!! ${warning}`, { color: 'yellow' }); + // Log at error level so the reason lands in error.log — the final summary's failure section + // points users to the error logs, and a bare warn would leave error.log empty. + log.error(warning, this.exportConfig.context); + // Surface the skip in the FINAL global summary without opening a live progress section + // (createNestedProgress would render an empty "ASSETS:" bar). See markAssetsSkippedInSummary. + this.markAssetsSkippedInSummary(warning); + return; + } + const linkedWorkspaces = this.exportConfig.linkedWorkspaces ?? []; if (linkedWorkspaces.length > 0) { diff --git a/packages/contentstack-export/src/types/export-config.ts b/packages/contentstack-export/src/types/export-config.ts index 893e2dc8e..33cf163c0 100644 --- a/packages/contentstack-export/src/types/export-config.ts +++ b/packages/contentstack-export/src/types/export-config.ts @@ -1,3 +1,4 @@ +import { FeatureStatus } from '@contentstack/cli-utilities'; import { Context, Modules, Region } from '.'; import DefaultConfig from './default-config'; @@ -36,6 +37,7 @@ export default interface ExportConfig extends DefaultConfig { skipDependencies?: boolean; authenticationMethod?: string; linkedWorkspaces?: Array<{ uid: string; space_uid: string; is_default: boolean }>; + planStatus?: Record; } type branch = { diff --git a/packages/contentstack-export/src/utils/export-config-handler.ts b/packages/contentstack-export/src/utils/export-config-handler.ts index 36c803e31..f47bd7912 100644 --- a/packages/contentstack-export/src/utils/export-config-handler.ts +++ b/packages/contentstack-export/src/utils/export-config-handler.ts @@ -1,6 +1,15 @@ import merge from 'merge'; import * as path from 'path'; -import { configHandler, isAuthenticated, cliux, sanitizePath, log } from '@contentstack/cli-utilities'; +import { + configHandler, + isAuthenticated, + cliux, + sanitizePath, + log, + isFeatureEnabled, + FeatureCtx, + FEATURE, +} from '@contentstack/cli-utilities'; import defaultConfig from '../config'; import { readFile, isDirectoryNonEmpty } from './file-helper'; import { askExportDir, askAPIKey } from './interactive'; @@ -8,7 +17,10 @@ import login from './basic-login'; import { filter, includes } from 'lodash'; import { ExportConfig } from '../types'; -const setupConfig = async (exportCmdFlags: any): Promise => { +const setupConfig = async ( + exportCmdFlags: any, + context?: { planCheckRequired?: string[] }, +): Promise => { // Set progress supported module FIRST, before any log calls // This ensures the logger respects the showConsoleLogs setting correctly configHandler.set('log.progressSupportedModule', 'export'); @@ -150,6 +162,31 @@ const setupConfig = async (exportCmdFlags: any): Promise => { } // Add authentication details to config for context tracking config.authenticationMethod = authenticationMethod; + + // DX-9314: detect AM 2.0 via the plan-check API. Unlike the CS Assets API (which rejects + // management tokens), the auth-api /feature-status endpoint accepts a management token, so this + // reliably tells us the org is AM 2.0 even under `--alias`. The assets module uses this to skip + // AM 2.0 asset export with a loud warning instead of silently emitting the legacy layout. + // `context.planCheckRequired` is honored if PR #2627's plan-guard prerun hook populated it, but + // we always check ASSET_MANAGEMENT directly so this works even without that hook. + const featuresToCheck: string[] = Array.from( + new Set([FEATURE.ASSET_MANAGEMENT, ...(context?.planCheckRequired ?? [])]), + ); + config.planStatus = config.planStatus || {}; + const planCtx: FeatureCtx = { + apiKey: config.apiKey, + managementToken: config.management_token, + authToken: config.auth_token, + }; + for (const featureUid of featuresToCheck) { + try { + config.planStatus[featureUid] = await isFeatureEnabled(featureUid, planCtx); + log.debug(`[export] Plan status fetched for "${featureUid}".`); + } catch (error) { + log.warn(`[export] Could not fetch plan status for "${featureUid}": ${(error as Error).message}`); + } + } + log.debug('Export configuration setup completed.', { ...config }); return config; From 36f8f8f85be47af3b0ae1b991350703ab6710040 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Tue, 28 Jul 2026 17:40:22 +0530 Subject: [PATCH 2/2] chore: resolve PR comments --- .talismanrc | 46 +------------- packages/contentstack-export/package.json | 5 +- .../src/export/modules/assets.ts | 61 ++++--------------- .../contentstack-export/src/types/index.ts | 15 +++++ .../src/utils/export-config-handler.ts | 51 ++++++++-------- 5 files changed, 57 insertions(+), 121 deletions(-) diff --git a/.talismanrc b/.talismanrc index 517bdf6d7..b8004aaf4 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,46 +1,4 @@ fileignoreconfig: - - filename: pnpm-lock.yaml - checksum: 8eb82c59ad8b81af64587af55c6788d09a34d1b651d030d1ef01dce472716f2a - - filename: packages/contentstack-branches/test/unit/helpers/stub-auth.ts - checksum: 8cafd5994d3ec13ba9af74c80b330bfd14721ea4e0359b456598964a6c2913ce - - filename: packages/contentstack-export/src/config/index.ts - checksum: 0219426b1873decef4d74bef5bfd50d1ed8f2567a2136122d869e4e53e4b4b99 - - filename: packages/contentstack-clone/README.md - checksum: fad666bf6290c980406ddd18caec2dd89d9c7a22b422010865199659956ab523 - - filename: packages/contentstack-migration/README.md - checksum: ceb7631888ee81711a32e2ac07762957bbf0f9c6c7f3f6bbc5d86326bd4352cc - - filename: packages/contentstack-external-migrate/test/commands/migrate/import.test.ts - checksum: 1d955eb34ab7e7e11cfad35cdbcb00692e39568bc690f46089fcbc0ca22a4852 - - filename: packages/contentstack-external-migrate/test/adapters/contentful/export.test.ts - checksum: 118cc140edf3b68191d1ef770c4142f9f5aa7af87a5a1832d6b3edee53d61c83 - - filename: packages/contentstack-cli-tsgen/test/unit/helper.test.ts - checksum: 146ff2a85a8f5ec463e51821f54c5f08143fa04209541a51017270e83b6ed46d - - filename: packages/contentstack-cli-cm-regex-validate/test/utils/connect-stack.test.ts - checksum: 018980aa2b919967b9ef9ab1bdf635d4867fe21593fba5890afa443f440228ff - - filename: packages/contentstack-bulk-operations/src/messages/index.ts - checksum: ba38daefd561d9acd018b6ebcb65847eceab6d375b28988d7b5624dcea9ccfe0 - - filename: packages/contentstack-bulk-operations/src/utils/operation-flag-matrix.ts - checksum: 99a3c3eb422a17f73f4c8f15088004fa4c95df3545bdf510310c1f3a20e4c2c2 - - filename: packages/contentstack-bulk-operations/src/base-bulk-command.ts - checksum: cb1147ac666b607a093c4c827d143d0bd2de5b6e9d59973f1b7052451e82a89b - - filename: packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts - checksum: 8bca423db4c3815c651ad922d986a53b6271cce3ea27f5987b66ee594151165e - - filename: packages/contentstack-bulk-operations/test/unit/commands/bulk-assets-init.test.ts - checksum: 590b1cfe42d46d0917ac90f363b1ccd05200b180bd9c58c770ffd1f12eb18327 - - filename: packages/contentstack-bulk-operations/test/unit/utils/operation-flag-matrix.test.ts - checksum: 35451ca4c03359b06531a8461091f4a3a45c4dea1f8853081fb53e4ce28c1cc3 - - filename: packages/contentstack-bulk-operations/test/unit/base-bulk-command.test.ts - checksum: ca699a9d73757d44ded4d9d27beb4f460f911a7c88a14551a9402a3ac0c69873 - - filename: packages/contentstack-external-migrate/src/lib/create-stack.ts - checksum: 68a9510db6f2746ac5006c091d276c1ba619a9e15c76a3edae3967e4f9c2dd4e - - filename: packages/contentstack-external-migrate/test/lib/create-stack.test.ts - checksum: 2dcbc359ee275e59e0536f3c325416eac8c43eb341b04da0d3757f5b5e556d9c - - filename: CHANGELOG.md - checksum: 88c7e1dee308fa4ae25e7815ead0842b1aac7ed669b02859cc8b25cba879595d - - filename: packages/contentstack-bulk-operations/test/unit/services/taxonomy-service.test.ts - checksum: abc5ac707341760cf59d5b8b1c4e13cf2c79955e2735c33e2db3ec6bc48eddb6 - - filename: packages/contentstack-import/src/import/modules/assets.ts - checksum: cda61a9c90bb39f27c09951b8a2623851296aefd0d3220d066032287a712d899 - - filename: packages/contentstack-asset-management/test/unit/utils/cs-assets-api-adapter.test.ts - checksum: 63c6bff4d51842d8fa3cce88545259d0a2c3cfe71df95d303d993f692cee883b +- filename: packages/contentstack-export/src/utils/export-config-handler.ts + checksum: ab7fd2fb63420e7a66856abace57965c1e9aa1033fc18dc8b194cf1d306f0bab version: '1.0' diff --git a/packages/contentstack-export/package.json b/packages/contentstack-export/package.json index 221252330..2e6af37f5 100644 --- a/packages/contentstack-export/package.json +++ b/packages/contentstack-export/package.json @@ -90,7 +90,10 @@ "csdxConfig": { "shortCommandName": { "cm:stacks:export": "EXPRT" - } + }, + "planProtectedFeatures": [ + "amAssets" + ] }, "repository": { "type": "git", diff --git a/packages/contentstack-export/src/export/modules/assets.ts b/packages/contentstack-export/src/export/modules/assets.ts index 3ea23492e..cca23f75a 100644 --- a/packages/contentstack-export/src/export/modules/assets.ts +++ b/packages/contentstack-export/src/export/modules/assets.ts @@ -25,7 +25,7 @@ import { import { PATH_CONSTANTS } from '../../constants'; import config from '../../config'; -import { ModuleClassParams } from '../../types'; +import { ModuleClassParams, GlobalSummary } from '../../types'; import BaseClass, { CustomPromiseHandler, CustomPromiseHandlerInput } from './base-class'; import { ExportSpaces, type AssetExportCounts } from '@contentstack/cli-asset-management'; import { @@ -38,23 +38,6 @@ import { } from '../../utils'; import { handle } from '@oclif/core'; -// cli-utilities' CLIProgressManager.globalSummary is runtime-accessible but typed private. These -// shapes describe the slice we mutate; the single accessor (getGlobalSummary) owns the structural -// cast + feature-detect so applyAssetSummaryCounts and markAssetsSkippedInSummary don't each copy it. -type SummaryModuleShape = { - status: string; - successCount: number; - failureCount: number; - failures: Array<{ item: string; error: string }>; - endTime?: number; -}; -type GlobalSummaryShape = { - getModules(): Map; - registerModule(name: string, totalItems?: number): void; - startModule(name: string): void; - completeModule(name: string, success?: boolean): void; -}; - export default class ExportAssets extends BaseClass { private assetsRootPath: string; public assetConfig = config.modules.assets; @@ -75,14 +58,10 @@ export default class ExportAssets extends BaseClass { }; } - /** - * cli-utilities' CLIProgressManager.globalSummary is runtime-accessible but typed private. Reach it - * via a single structural cast + feature-detect here so both callers share one copy of the - * private-shape assumption; returns null (caller degrades) if a future cli-utilities version - * changes the shape. - */ - private getGlobalSummary(): GlobalSummaryShape | null { - const gs = (CLIProgressManager as unknown as { globalSummary?: GlobalSummaryShape | null }).globalSummary; + // globalSummary is runtime-accessible but typed private; feature-detect the shape and return null + // so callers degrade instead of throwing if a future cli-utilities version changes it. + private getGlobalSummary(): GlobalSummary | null { + const gs = (CLIProgressManager as unknown as { globalSummary?: GlobalSummary | null }).globalSummary; if ( !gs || typeof gs.getModules !== 'function' || @@ -138,15 +117,9 @@ export default class ExportAssets extends BaseClass { } } - /** - * DX-9314 — record the AM 2.0 asset skip in the FINAL export summary without a live progress bar. - * We do NOT call createNestedProgress (it would print an empty "ASSETS:" section); instead we reach - * the global summary directly (same structural cast + feature-detect + degrade pattern as - * applyAssetSummaryCounts) and push a failure entry so the Module Details row shows `✗ ASSETS` and the - * Failure Summary prints the reason verbatim. failureCount stays 0 so the row reads `0/0 items`. - * NOTE: this reuses the failure channel deliberately — a first-class "skipped" summary status is a - * separate cli-utilities ticket. - */ + // Records the skip in the final summary via the failure channel, which is the only per-module + // slot carrying a message. createNestedProgress is intentionally not used: it would render an + // empty "ASSETS:" live section. private markAssetsSkippedInSummary(reason: string): void { const gs = this.getGlobalSummary(); if (!gs) { @@ -170,24 +143,14 @@ export default class ExportAssets extends BaseClass { } async start(): Promise { - // AM 2.0 assets cannot be exported with a management token — the CS Assets API only - // accepts a logged-in session (auth token / OAuth). Today the linked-workspaces lookup silently - // returns [] under a management token, so the export falls back to the legacy layout and import - // later reports success with broken entry->asset references. Detect the AM 2.0 org up front via - // the plan-check (which DOES accept a management token) and skip the assets module with a loud - // warning instead of silently emitting the wrong layout. - const amInPlan = this.exportConfig.planStatus?.[FEATURE.ASSET_MANAGEMENT]?.is_part_of_plan; - if (amInPlan && this.exportConfig.management_token) { + const csAssetsInPlan = this.exportConfig.planStatus?.[FEATURE.ASSET_MANAGEMENT]?.is_part_of_plan; + if (csAssetsInPlan && this.exportConfig.management_token) { const warning = - 'Skipping AM 2.0 asset export: management token authentication is not supported by the Assets APIs. ' + + 'Skipping Contentstack Assets export: management token authentication is not supported by the Assets APIs. ' + 'Entry-to-asset references will NOT resolve in the exported content. ' + - 'Re-run the export with a logged-in session (auth token or OAuth) to export AM 2.0 assets.'; + 'Re-run the export with a logged-in session (auth token or OAuth) to export Contentstack Assets.'; cliux.print(`\nWARNING!!! ${warning}`, { color: 'yellow' }); - // Log at error level so the reason lands in error.log — the final summary's failure section - // points users to the error logs, and a bare warn would leave error.log empty. log.error(warning, this.exportConfig.context); - // Surface the skip in the FINAL global summary without opening a live progress section - // (createNestedProgress would render an empty "ASSETS:" bar). See markAssetsSkippedInSummary. this.markAssetsSkippedInSummary(warning); return; } diff --git a/packages/contentstack-export/src/types/index.ts b/packages/contentstack-export/src/types/index.ts index 605129e48..9694b46fd 100644 --- a/packages/contentstack-export/src/types/index.ts +++ b/packages/contentstack-export/src/types/index.ts @@ -180,6 +180,21 @@ export interface Context { authenticationMethod?: string; } +export interface SummaryModule { + status: string; + successCount: number; + failureCount: number; + failures: Array<{ item: string; error: string }>; + endTime?: number; +} + +export interface GlobalSummary { + getModules(): Map; + registerModule(name: string, totalItems?: number): void; + startModule(name: string): void; + completeModule(name: string, success?: boolean): void; +} + export { default as DefaultConfig } from './default-config'; export { default as ExportConfig } from './export-config'; export * from './marketplace-app'; diff --git a/packages/contentstack-export/src/utils/export-config-handler.ts b/packages/contentstack-export/src/utils/export-config-handler.ts index f47bd7912..04321613d 100644 --- a/packages/contentstack-export/src/utils/export-config-handler.ts +++ b/packages/contentstack-export/src/utils/export-config-handler.ts @@ -8,7 +8,6 @@ import { log, isFeatureEnabled, FeatureCtx, - FEATURE, } from '@contentstack/cli-utilities'; import defaultConfig from '../config'; import { readFile, isDirectoryNonEmpty } from './file-helper'; @@ -17,10 +16,7 @@ import login from './basic-login'; import { filter, includes } from 'lodash'; import { ExportConfig } from '../types'; -const setupConfig = async ( - exportCmdFlags: any, - context?: { planCheckRequired?: string[] }, -): Promise => { +const setupConfig = async (exportCmdFlags: any, context: any): Promise => { // Set progress supported module FIRST, before any log calls // This ensures the logger respects the showConsoleLogs setting correctly configHandler.set('log.progressSupportedModule', 'export'); @@ -162,32 +158,33 @@ const setupConfig = async ( } // Add authentication details to config for context tracking config.authenticationMethod = authenticationMethod; + log.debug('Export configuration setup completed.', { ...config }); - // DX-9314: detect AM 2.0 via the plan-check API. Unlike the CS Assets API (which rejects - // management tokens), the auth-api /feature-status endpoint accepts a management token, so this - // reliably tells us the org is AM 2.0 even under `--alias`. The assets module uses this to skip - // AM 2.0 asset export with a loud warning instead of silently emitting the legacy layout. - // `context.planCheckRequired` is honored if PR #2627's plan-guard prerun hook populated it, but - // we always check ASSET_MANAGEMENT directly so this works even without that hook. - const featuresToCheck: string[] = Array.from( - new Set([FEATURE.ASSET_MANAGEMENT, ...(context?.planCheckRequired ?? [])]), - ); - config.planStatus = config.planStatus || {}; - const planCtx: FeatureCtx = { - apiKey: config.apiKey, - managementToken: config.management_token, - authToken: config.auth_token, - }; - for (const featureUid of featuresToCheck) { - try { - config.planStatus[featureUid] = await isFeatureEnabled(featureUid, planCtx); - log.debug(`[export] Plan status fetched for "${featureUid}".`); - } catch (error) { - log.warn(`[export] Could not fetch plan status for "${featureUid}": ${(error as Error).message}`); + // Deferred plan check — credentials now available after setupExportConfig + const deferredFeatures: string[] = context?.planCheckRequired ?? []; + if (deferredFeatures.length > 0) { + const planCtx: FeatureCtx = { + apiKey: config.apiKey, + managementToken: config.management_token, + authToken: config.auth_token, + }; + for (const featureUid of deferredFeatures) { + try { + const status = await isFeatureEnabled(featureUid, planCtx); + if (context) { + context.planStatus[featureUid] = status; + } + + log.debug(`[export] Deferred plan status fetched for "${featureUid}".`); + } catch (error) { + log.warn(`[export] Could not fetch deferred plan status for "${featureUid}": ${(error as Error).message}`); + } } } - log.debug('Export configuration setup completed.', { ...config }); + if (context?.planStatus) { + config.planStatus = context.planStatus; + } return config; };