11import find from 'lodash/find' ;
2+ import omit from 'lodash/omit' ;
23import { resolve as pResolve } from 'node:path' ;
34import {
45 handleAndLogError ,
@@ -43,38 +44,35 @@ export default class ExportStack extends BaseClass {
4344 try {
4445 log . debug ( 'Starting stack export process...' , this . exportConfig . context ) ;
4546
46- // Initial analysis with loading spinner
47+ // Initial analysis with loading spinner (skip getStack when using management token — no SDK snapshot)
4748 const [ stackData ] = await this . withLoadingSpinner ( 'STACK: Analyzing stack configuration...' , async ( ) => {
48- const stackData = isAuthenticated ( ) ? await this . getStack ( ) : null ;
49+ const stackData =
50+ this . exportConfig . management_token || ! isAuthenticated ( ) ? null : await this . getStack ( ) ;
4951 return [ stackData ] ;
5052 } ) ;
5153
5254 // Create nested progress manager
5355 const progress = this . createNestedProgress ( this . currentModuleName ) ;
5456
55- // Add processes based on configuration
56- let processCount = 0 ;
57-
58- if ( stackData ?. org_uid ) {
59- log . debug ( `Found organization UID: '${ stackData . org_uid } '.` , this . exportConfig . context ) ;
60- this . exportConfig . org_uid = stackData . org_uid ;
57+ const orgUid = stackData ?. org_uid ?? stackData ?. organization_uid ;
58+ if ( orgUid ) {
59+ log . debug ( `Found organization UID: '${ orgUid } '.` , this . exportConfig . context ) ;
60+ this . exportConfig . org_uid = orgUid ;
6161 this . exportConfig . sourceStackName = stackData . name ;
6262 log . debug ( `Set source stack name: ${ stackData . name } ` , this . exportConfig . context ) ;
6363 }
6464
6565 if ( ! this . exportConfig . management_token ) {
6666 progress . addProcess ( PROCESS_NAMES . STACK_SETTINGS , 1 ) ;
67- processCount ++ ;
6867 }
68+ progress . addProcess ( PROCESS_NAMES . STACK_DETAILS , 1 ) ;
6969
7070 if ( ! this . exportConfig . preserveStackVersion && ! this . exportConfig . hasOwnProperty ( 'master_locale' ) ) {
7171 progress . addProcess ( PROCESS_NAMES . STACK_LOCALE , 1 ) ;
72- processCount ++ ;
73- } else if ( this . exportConfig . preserveStackVersion ) {
74- progress . addProcess ( PROCESS_NAMES . STACK_DETAILS , 1 ) ;
75- processCount ++ ;
7672 }
7773
74+ let stackDetailsExportResult : any ;
75+
7876 // Execute processes
7977 if ( ! this . exportConfig . management_token ) {
8078 progress
@@ -85,11 +83,28 @@ export default class ExportStack extends BaseClass {
8583 ) ;
8684 await this . exportStackSettings ( ) ;
8785 progress . completeProcess ( PROCESS_NAMES . STACK_SETTINGS , true ) ;
86+
87+ progress
88+ . startProcess ( PROCESS_NAMES . STACK_DETAILS )
89+ . updateStatus (
90+ PROCESS_STATUS [ PROCESS_NAMES . STACK_DETAILS ] . EXPORTING ,
91+ PROCESS_NAMES . STACK_DETAILS ,
92+ ) ;
93+ stackDetailsExportResult = await this . exportStack ( stackData ) ;
94+ progress . completeProcess ( PROCESS_NAMES . STACK_DETAILS , true ) ;
8895 } else {
8996 log . info (
9097 'Skipping stack settings export: Operation is not supported when using a management token.' ,
9198 this . exportConfig . context ,
9299 ) ;
100+ progress
101+ . startProcess ( PROCESS_NAMES . STACK_DETAILS )
102+ . updateStatus (
103+ PROCESS_STATUS [ PROCESS_NAMES . STACK_DETAILS ] . EXPORTING ,
104+ PROCESS_NAMES . STACK_DETAILS ,
105+ ) ;
106+ stackDetailsExportResult = await this . writeStackJsonFromConfigApiKeyOnly ( ) ;
107+ progress . completeProcess ( PROCESS_NAMES . STACK_DETAILS , true ) ;
93108 }
94109
95110 if ( ! this . exportConfig . preserveStackVersion && ! this . exportConfig . hasOwnProperty ( 'master_locale' ) ) {
@@ -110,17 +125,8 @@ export default class ExportStack extends BaseClass {
110125 this . completeProgress ( true ) ;
111126 return masterLocale ;
112127 } else if ( this . exportConfig . preserveStackVersion ) {
113- progress
114- . startProcess ( PROCESS_NAMES . STACK_DETAILS )
115- . updateStatus (
116- PROCESS_STATUS [ PROCESS_NAMES . STACK_DETAILS ] . EXPORTING ,
117- PROCESS_NAMES . STACK_DETAILS ,
118- ) ;
119- const stackResult = await this . exportStack ( ) ;
120- progress . completeProcess ( PROCESS_NAMES . STACK_DETAILS , true ) ;
121-
122128 this . completeProgress ( true ) ;
123- return stackResult ;
129+ return stackDetailsExportResult ;
124130 } else {
125131 log . debug ( 'Locale locale already set, skipping locale fetch' , this . exportConfig . context ) ;
126132 }
@@ -225,33 +231,36 @@ export default class ExportStack extends BaseClass {
225231 } ) ;
226232 }
227233
228- async exportStack ( ) : Promise < any > {
234+ /**
235+ * Reuse stack snapshot from `getStack()` when present so we do not call `stack.fetch()` twice
236+ * (same GET /stacks payload as writing stack.json). Falls back to `this.stack.fetch()` otherwise.
237+ */
238+ async exportStack ( preloadedStack ?: Record < string , any > | null ) : Promise < any > {
229239 log . debug ( `Starting stack export for: '${ this . exportConfig . apiKey } '...` , this . exportConfig . context ) ;
230240
231241 await fsUtil . makeDirectory ( this . stackFolderPath ) ;
232242 log . debug ( `Created stack directory at: '${ this . stackFolderPath } '` , this . exportConfig . context ) ;
233243
234- return this . stack
235- . fetch ( )
236- . then ( ( resp : any ) => {
237- const stackFilePath = pResolve ( this . stackFolderPath , this . stackConfig . fileName ) ;
238- log . debug ( `Writing stack data to: '${ stackFilePath } '` , this . exportConfig . context ) ;
239- fsUtil . writeFile ( stackFilePath , resp ) ;
240-
241- // Track progress for stack export completion
244+ if ( this . isStackFetchPayload ( preloadedStack ) ) {
245+ log . debug ( 'Reusing stack payload from analysis step (no extra stack.fetch).' , this . exportConfig . context ) ;
246+ try {
247+ return this . persistStackJsonPayload ( preloadedStack ) ;
248+ } catch ( error : any ) {
242249 this . progressManager ?. tick (
243- true ,
244- ` stack: ${ this . exportConfig . apiKey } ` ,
245- null ,
250+ false ,
251+ ' stack export' ,
252+ error ?. message || PROCESS_STATUS [ PROCESS_NAMES . STACK_DETAILS ] . FAILED ,
246253 PROCESS_NAMES . STACK_DETAILS ,
247254 ) ;
255+ handleAndLogError ( error , { ...this . exportConfig . context } ) ;
256+ return undefined ;
257+ }
258+ }
248259
249- log . success (
250- `Stack details exported successfully for stack ${ this . exportConfig . apiKey } ` ,
251- this . exportConfig . context ,
252- ) ;
253- log . debug ( 'Stack export completed successfully.' , this . exportConfig . context ) ;
254- return resp ;
260+ return this . stack
261+ . fetch ( )
262+ . then ( ( resp : any ) => {
263+ return this . persistStackJsonPayload ( resp ) ;
255264 } )
256265 . catch ( ( error : any ) => {
257266 log . debug ( `Error occurred while exporting stack: ${ this . exportConfig . apiKey } ` , this . exportConfig . context ) ;
@@ -265,6 +274,65 @@ export default class ExportStack extends BaseClass {
265274 } ) ;
266275 }
267276
277+ private isStackFetchPayload ( data : unknown ) : data is Record < string , any > {
278+ return (
279+ typeof data === 'object' &&
280+ data !== null &&
281+ ! Array . isArray ( data ) &&
282+ ( 'api_key' in data || 'uid' in data )
283+ ) ;
284+ }
285+
286+ /**
287+ * Management-token exports cannot use Stack CMA endpoints for full metadata; write api_key from config only.
288+ */
289+ private async writeStackJsonFromConfigApiKeyOnly ( ) : Promise < { api_key : string } > {
290+ if ( ! this . exportConfig . apiKey || typeof this . exportConfig . apiKey !== 'string' ) {
291+ throw new Error ( 'Stack API key is required to write stack.json when using a management token.' ) ;
292+ }
293+
294+ log . debug ( 'Writing config-based stack.json (api_key only, no stack fetch).' , this . exportConfig . context ) ;
295+
296+ await fsUtil . makeDirectory ( this . stackFolderPath ) ;
297+ const payload = { api_key : this . exportConfig . apiKey } ;
298+ const stackFilePath = pResolve ( this . stackFolderPath , this . stackConfig . fileName ) ;
299+ fsUtil . writeFile ( stackFilePath , payload ) ;
300+
301+ this . progressManager ?. tick (
302+ true ,
303+ `stack: ${ this . exportConfig . apiKey } ` ,
304+ null ,
305+ PROCESS_NAMES . STACK_DETAILS ,
306+ ) ;
307+
308+ log . success (
309+ `Stack identifier written to stack.json from config for stack ${ this . exportConfig . apiKey } ` ,
310+ this . exportConfig . context ,
311+ ) ;
312+ return payload ;
313+ }
314+
315+ private persistStackJsonPayload ( resp : Record < string , any > ) : any {
316+ const sanitized = omit ( resp , this . stackConfig . invalidKeys ?? [ ] ) ;
317+ const stackFilePath = pResolve ( this . stackFolderPath , this . stackConfig . fileName ) ;
318+ log . debug ( `Writing stack data to: '${ stackFilePath } '` , this . exportConfig . context ) ;
319+ fsUtil . writeFile ( stackFilePath , sanitized ) ;
320+
321+ this . progressManager ?. tick (
322+ true ,
323+ `stack: ${ this . exportConfig . apiKey } ` ,
324+ null ,
325+ PROCESS_NAMES . STACK_DETAILS ,
326+ ) ;
327+
328+ log . success (
329+ `Stack details exported successfully for stack ${ this . exportConfig . apiKey } ` ,
330+ this . exportConfig . context ,
331+ ) ;
332+ log . debug ( 'Stack export completed successfully.' , this . exportConfig . context ) ;
333+ return sanitized ;
334+ }
335+
268336 async exportStackSettings ( ) : Promise < any > {
269337 log . info ( 'Exporting stack settings...' , this . exportConfig . context ) ;
270338 await fsUtil . makeDirectory ( this . stackFolderPath ) ;
0 commit comments