11import find from 'lodash/find' ;
2+ import omit from 'lodash/omit' ;
23import { resolve as pResolve } from 'node:path' ;
34import { handleAndLogError , isAuthenticated , managementSDKClient , log } from '@contentstack/cli-utilities' ;
45import { PATH_CONSTANTS } from '../../constants' ;
@@ -39,50 +40,57 @@ export default class ExportStack extends BaseClass {
3940 try {
4041 log . debug ( 'Starting stack export process...' , this . exportConfig . context ) ;
4142
42- // Initial analysis with loading spinner
43+ // Initial analysis with loading spinner (skip getStack when using management token — no SDK snapshot)
4344 const [ stackData ] = await this . withLoadingSpinner ( 'STACK: Analyzing stack configuration...' , async ( ) => {
44- const stackData = isAuthenticated ( ) ? await this . getStack ( ) : null ;
45+ const stackData = this . exportConfig . management_token || ! isAuthenticated ( ) ? null : await this . getStack ( ) ;
4546 return [ stackData ] ;
4647 } ) ;
4748
4849 // Create nested progress manager
4950 const progress = this . createNestedProgress ( this . currentModuleName ) ;
5051
51- // Add processes based on configuration
52- let processCount = 0 ;
53-
54- if ( stackData ?. org_uid ) {
55- log . debug ( `Found organization UID: '${ stackData . org_uid } '.` , this . exportConfig . context ) ;
56- this . exportConfig . org_uid = stackData . org_uid ;
52+ const orgUid = stackData ?. org_uid ?? stackData ?. organization_uid ;
53+ if ( orgUid ) {
54+ log . debug ( `Found organization UID: '${ orgUid } '.` , this . exportConfig . context ) ;
55+ this . exportConfig . org_uid = orgUid ;
5756 this . exportConfig . sourceStackName = stackData . name ;
5857 log . debug ( `Set source stack name: ${ stackData . name } ` , this . exportConfig . context ) ;
5958 }
6059
6160 if ( ! this . exportConfig . management_token ) {
6261 progress . addProcess ( PROCESS_NAMES . STACK_SETTINGS , 1 ) ;
63- processCount ++ ;
6462 }
63+ progress . addProcess ( PROCESS_NAMES . STACK_DETAILS , 1 ) ;
6564
6665 if ( ! this . exportConfig . preserveStackVersion && ! this . exportConfig . hasOwnProperty ( 'master_locale' ) ) {
6766 progress . addProcess ( PROCESS_NAMES . STACK_LOCALE , 1 ) ;
68- processCount ++ ;
69- } else if ( this . exportConfig . preserveStackVersion ) {
70- progress . addProcess ( PROCESS_NAMES . STACK_DETAILS , 1 ) ;
71- processCount ++ ;
7267 }
7368
69+ let stackDetailsExportResult : any ;
70+
7471 // Execute processes
7572 if ( ! this . exportConfig . management_token ) {
7673 progress
7774 . startProcess ( PROCESS_NAMES . STACK_SETTINGS )
7875 . updateStatus ( PROCESS_STATUS [ PROCESS_NAMES . STACK_SETTINGS ] . EXPORTING , PROCESS_NAMES . STACK_SETTINGS ) ;
7976 await this . exportStackSettings ( ) ;
8077 progress . completeProcess ( PROCESS_NAMES . STACK_SETTINGS , true ) ;
78+
79+ progress
80+ . startProcess ( PROCESS_NAMES . STACK_DETAILS )
81+ . updateStatus ( PROCESS_STATUS [ PROCESS_NAMES . STACK_DETAILS ] . EXPORTING , PROCESS_NAMES . STACK_DETAILS ) ;
82+ stackDetailsExportResult = await this . exportStack ( stackData ) ;
83+ progress . completeProcess ( PROCESS_NAMES . STACK_DETAILS , true ) ;
8184 } else {
8285 log . info (
8386 'Skipping stack settings export: Operation is not supported when using a management token.' ,
8487 this . exportConfig . context ,
8588 ) ;
89+ progress
90+ . startProcess ( PROCESS_NAMES . STACK_DETAILS )
91+ . updateStatus ( PROCESS_STATUS [ PROCESS_NAMES . STACK_DETAILS ] . EXPORTING , PROCESS_NAMES . STACK_DETAILS ) ;
92+ stackDetailsExportResult = await this . writeStackJsonFromConfigApiKeyOnly ( ) ;
93+ progress . completeProcess ( PROCESS_NAMES . STACK_DETAILS , true ) ;
8694 }
8795
8896 if ( ! this . exportConfig . preserveStackVersion && ! this . exportConfig . hasOwnProperty ( 'master_locale' ) ) {
@@ -100,14 +108,8 @@ export default class ExportStack extends BaseClass {
100108 this . completeProgress ( true ) ;
101109 return masterLocale ;
102110 } else if ( this . exportConfig . preserveStackVersion ) {
103- progress
104- . startProcess ( PROCESS_NAMES . STACK_DETAILS )
105- . updateStatus ( PROCESS_STATUS [ PROCESS_NAMES . STACK_DETAILS ] . EXPORTING , PROCESS_NAMES . STACK_DETAILS ) ;
106- const stackResult = await this . exportStack ( ) ;
107- progress . completeProcess ( PROCESS_NAMES . STACK_DETAILS , true ) ;
108-
109111 this . completeProgress ( true ) ;
110- return stackResult ;
112+ return stackDetailsExportResult ;
111113 } else {
112114 log . debug ( 'Locale locale already set, skipping locale fetch' , this . exportConfig . context ) ;
113115 }
@@ -211,28 +213,36 @@ export default class ExportStack extends BaseClass {
211213 } ) ;
212214 }
213215
214- async exportStack ( ) : Promise < any > {
216+ /**
217+ * Reuse stack snapshot from `getStack()` when present so we do not call `stack.fetch()` twice
218+ * (same GET /stacks payload as writing stack.json). Falls back to `this.stack.fetch()` otherwise.
219+ */
220+ async exportStack ( preloadedStack ?: Record < string , any > | null ) : Promise < any > {
215221 log . debug ( `Starting stack export for: '${ this . exportConfig . apiKey } '...` , this . exportConfig . context ) ;
216222
217223 await fsUtil . makeDirectory ( this . stackFolderPath ) ;
218224 log . debug ( `Created stack directory at: '${ this . stackFolderPath } '` , this . exportConfig . context ) ;
219225
226+ if ( this . isStackFetchPayload ( preloadedStack ) ) {
227+ log . debug ( 'Reusing stack payload from analysis step (no extra stack.fetch).' , this . exportConfig . context ) ;
228+ try {
229+ return this . persistStackJsonPayload ( preloadedStack ) ;
230+ } catch ( error : any ) {
231+ this . progressManager ?. tick (
232+ false ,
233+ 'stack export' ,
234+ error ?. message || PROCESS_STATUS [ PROCESS_NAMES . STACK_DETAILS ] . FAILED ,
235+ PROCESS_NAMES . STACK_DETAILS ,
236+ ) ;
237+ handleAndLogError ( error , { ...this . exportConfig . context } ) ;
238+ return undefined ;
239+ }
240+ }
241+
220242 return this . stack
221243 . fetch ( )
222- . then ( async ( resp : any ) => {
223- const stackFilePath = pResolve ( this . stackFolderPath , this . stackConfig . fileName ) ;
224- log . debug ( `Writing stack data to: '${ stackFilePath } '` , this . exportConfig . context ) ;
225- fsUtil . writeFile ( stackFilePath , resp ) ;
226-
227- // Track progress for stack export completion
228- this . progressManager ?. tick ( true , `stack: ${ this . exportConfig . apiKey } ` , null , PROCESS_NAMES . STACK_DETAILS ) ;
229-
230- log . success (
231- `Stack details exported successfully for stack ${ this . exportConfig . apiKey } ` ,
232- this . exportConfig . context ,
233- ) ;
234- log . debug ( 'Stack export completed successfully.' , this . exportConfig . context ) ;
235- return resp ;
244+ . then ( ( resp : any ) => {
245+ return this . persistStackJsonPayload ( resp ) ;
236246 } )
237247 . catch ( ( error : any ) => {
238248 log . debug ( `Error occurred while exporting stack: ${ this . exportConfig . apiKey } ` , this . exportConfig . context ) ;
@@ -246,6 +256,47 @@ export default class ExportStack extends BaseClass {
246256 } ) ;
247257 }
248258
259+ private isStackFetchPayload ( data : unknown ) : data is Record < string , any > {
260+ return typeof data === 'object' && data !== null && ! Array . isArray ( data ) && ( 'api_key' in data || 'uid' in data ) ;
261+ }
262+
263+ /**
264+ * Management-token exports cannot use Stack CMA endpoints for full metadata; write api_key from config only.
265+ */
266+ private async writeStackJsonFromConfigApiKeyOnly ( ) : Promise < { api_key : string } > {
267+ if ( ! this . exportConfig . apiKey || typeof this . exportConfig . apiKey !== 'string' ) {
268+ throw new Error ( 'Stack API key is required to write stack.json when using a management token.' ) ;
269+ }
270+
271+ log . debug ( 'Writing config-based stack.json (api_key only, no stack fetch).' , this . exportConfig . context ) ;
272+
273+ await fsUtil . makeDirectory ( this . stackFolderPath ) ;
274+ const payload = { api_key : this . exportConfig . apiKey } ;
275+ const stackFilePath = pResolve ( this . stackFolderPath , this . stackConfig . fileName ) ;
276+ fsUtil . writeFile ( stackFilePath , payload ) ;
277+
278+ this . progressManager ?. tick ( true , `stack: ${ this . exportConfig . apiKey } ` , null , PROCESS_NAMES . STACK_DETAILS ) ;
279+
280+ log . success (
281+ `Stack identifier written to stack.json from config for stack ${ this . exportConfig . apiKey } ` ,
282+ this . exportConfig . context ,
283+ ) ;
284+ return payload ;
285+ }
286+
287+ private persistStackJsonPayload ( resp : Record < string , any > ) : any {
288+ const sanitized = omit ( resp , this . stackConfig . invalidKeys ?? [ ] ) ;
289+ const stackFilePath = pResolve ( this . stackFolderPath , this . stackConfig . fileName ) ;
290+ log . debug ( `Writing stack data to: '${ stackFilePath } '` , this . exportConfig . context ) ;
291+ fsUtil . writeFile ( stackFilePath , sanitized ) ;
292+
293+ this . progressManager ?. tick ( true , `stack: ${ this . exportConfig . apiKey } ` , null , PROCESS_NAMES . STACK_DETAILS ) ;
294+
295+ log . success ( `Stack details exported successfully for stack ${ this . exportConfig . apiKey } ` , this . exportConfig . context ) ;
296+ log . debug ( 'Stack export completed successfully.' , this . exportConfig . context ) ;
297+ return sanitized ;
298+ }
299+
249300 async exportStackSettings ( ) : Promise < any > {
250301 log . info ( 'Exporting stack settings...' , this . exportConfig . context ) ;
251302 await fsUtil . makeDirectory ( this . stackFolderPath ) ;
0 commit comments