diff --git a/.talismanrc b/.talismanrc index 587a2037..70d1cc27 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,14 +1,4 @@ fileignoreconfig: - - filename: pnpm-lock.yaml - checksum: 069d87fc69d059bd53fa46d98916e831641ea4889cebcc9b9b30884ac67dab30 - - filename: packages/contentstack-branches/README.md - checksum: ad32bd365db7f085cc2ea133d69748954606131ec6157a272a3471aea60011c2 - - filename: packages/contentstack-branches/src/branch/diff-handler.ts - checksum: 3cd4d26a2142cab7cbf2094c9251e028467d17d6a1ed6daf22f21975133805f1 - - filename: packages/contentstack-branches/src/commands/cm/branches/merge-status.ts - checksum: 6e5b959ddcc5ff68e03c066ea185fcf6c6e57b1819069730340af35aad8a93a8 - - filename: packages/contentstack-branches/src/utils/create-branch.ts - checksum: d0613295ee26f7a77d026e40db0a4ab726fabd0a74965f729f1a66d1ef14768f - - filename: packages/contentstack-branches/src/branch/merge-handler.ts - checksum: 4fd8dba9b723733530b9ba12e81e1d3e5d60b73ac4c082defb10593f257bb133 + - filename: packages/contentstack-import/src/utils/import-config-handler.ts + checksum: 3194f537cee8041f07a7ea91cdc6351c84e400766696d9c3cf80b98f99961f76 version: '1.0' diff --git a/packages/contentstack-asset-management/src/export/spaces.ts b/packages/contentstack-asset-management/src/export/spaces.ts index 5d639a41..24a5a088 100644 --- a/packages/contentstack-asset-management/src/export/spaces.ts +++ b/packages/contentstack-asset-management/src/export/spaces.ts @@ -51,7 +51,7 @@ export class ExportSpaces { log.debug(`Exporting Asset Management 2.0 (${linkedWorkspaces.length} space(s))`, context); log.debug(`Spaces: ${linkedWorkspaces.map((ws) => ws.space_uid).join(', ')}`, context); - const spacesRootPath = pResolve(exportDir, branchName || 'main', 'spaces'); + const spacesRootPath = pResolve(exportDir, 'spaces'); await mkdir(spacesRootPath, { recursive: true }); log.debug(`Spaces root path: ${spacesRootPath}`, context); diff --git a/packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts b/packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts index b5398c81..7e3bc0cb 100644 --- a/packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts +++ b/packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts @@ -53,6 +53,41 @@ export class AssetManagementAdapter implements IAssetManagementAdapter { return '?' + parts.join('&'); } + /** + * Format response body or payload for error logging. Safely stringifies and truncates. + */ + private formatResponseBodyForError(data: unknown, maxLen: number = 500): string { + if (data === null || data === undefined) return ''; + try { + const str = typeof data === 'string' ? data : JSON.stringify(data); + return str.length > maxLen ? str.substring(0, maxLen) + '...' : str; + } catch { + return ''; + } + } + + /** + * Normalize AM API failures into a consistent error message with optional cause and body snippet. + */ + private normalizeAmGetFailure(details: { + path: string; + fullPath: string; + status?: number; + cause?: unknown; + bodySnippet?: string; + }): Error { + const { path, status, cause, bodySnippet } = details; + let message = `AM API GET failed: path ${path}`; + if (status) message += ` (status ${status})`; + if (cause && cause instanceof Error) { + message += ` - ${cause.message}`; + } else if (cause) { + message += ` - ${String(cause)}`; + } + if (bodySnippet) message += `\nResponse: ${bodySnippet}`; + return new Error(message); + } + /** * GET a space-level endpoint (e.g. /api/spaces/{uid}). Builds path + query string and performs the request. */ @@ -73,11 +108,29 @@ export class AssetManagementAdapter implements IAssetManagementAdapter { const queryString = this.buildQueryString(safeParams); const fullPath = path + queryString; log.debug(`GET ${fullPath}`, this.config.context); - const response = await this.apiClient.get(fullPath); - if (response.status < 200 || response.status >= 300) { - throw new Error(`Asset Management API error: status ${response.status}, path ${path}`); + + try { + const response = await this.apiClient.get(fullPath); + if (response.status < 200 || response.status >= 300) { + const bodySnippet = this.formatResponseBodyForError(response.data); + throw this.normalizeAmGetFailure({ + path, + fullPath, + status: response.status, + bodySnippet: bodySnippet || undefined, + }); + } + return response.data as T; + } catch (error) { + if (error instanceof Error && error.message.includes('AM API GET failed')) { + throw error; + } + throw this.normalizeAmGetFailure({ + path, + fullPath, + cause: error, + }); } - return response.data as T; } async init(): Promise { @@ -196,32 +249,60 @@ export class AssetManagementAdapter implements IAssetManagementAdapter { const baseUrl = this.config.baseURL?.replace(/\/$/, '') ?? ''; const headers = await this.getPostHeaders({ 'Content-Type': 'application/json', ...extraHeaders }); log.debug(`POST ${path}`, this.config.context); - const response = await fetch(`${baseUrl}${path}`, { - method: 'POST', - headers, - body: JSON.stringify(body), - }); - if (!response.ok) { - const text = await response.text().catch(() => ''); - throw new Error(`AM API POST error: status ${response.status}, path ${path}, body: ${text}`); + + try { + const response = await fetch(`${baseUrl}${path}`, { + method: 'POST', + headers, + body: JSON.stringify(body), + }); + if (!response.ok) { + const text = await response.text().catch(() => ''); + const bodySnippet = this.formatResponseBodyForError(text); + throw new Error( + `AM API POST failed: status ${response.status} path ${path}${ + bodySnippet ? `\nResponse: ${bodySnippet}` : '' + }`, + ); + } + return response.json() as Promise; + } catch (error) { + if (error instanceof Error && error.message.includes('AM API POST failed')) { + throw error; + } + throw new Error(`AM API POST failed: path ${path} - ${error instanceof Error ? error.message : String(error)}`); } - return response.json() as Promise; } private async postMultipart(path: string, form: FormData, extraHeaders: Record = {}): Promise { const baseUrl = this.config.baseURL?.replace(/\/$/, '') ?? ''; const headers = await this.getPostHeaders(extraHeaders); log.debug(`POST (multipart) ${path}`, this.config.context); - const response = await fetch(`${baseUrl}${path}`, { - method: 'POST', - headers, - body: form, - }); - if (!response.ok) { - const text = await response.text().catch(() => ''); - throw new Error(`AM API multipart POST error: status ${response.status}, path ${path}, body: ${text}`); + + try { + const response = await fetch(`${baseUrl}${path}`, { + method: 'POST', + headers, + body: form, + }); + if (!response.ok) { + const text = await response.text().catch(() => ''); + const bodySnippet = this.formatResponseBodyForError(text); + throw new Error( + `AM API multipart POST failed: status ${response.status} path ${path}${ + bodySnippet ? `\nResponse: ${bodySnippet}` : '' + }`, + ); + } + return response.json() as Promise; + } catch (error) { + if (error instanceof Error && error.message.includes('AM API multipart POST failed')) { + throw error; + } + throw new Error( + `AM API multipart POST failed: path ${path} - ${error instanceof Error ? error.message : String(error)}`, + ); } - return response.json() as Promise; } // --------------------------------------------------------------------------- diff --git a/packages/contentstack-export/src/export/module-exporter.ts b/packages/contentstack-export/src/export/module-exporter.ts index 7bd81f81..0a1dc4b5 100644 --- a/packages/contentstack-export/src/export/module-exporter.ts +++ b/packages/contentstack-export/src/export/module-exporter.ts @@ -83,12 +83,11 @@ class ModuleExporter { this.exportConfig.context, ); } catch (error) { - handleAndLogError( - error, - { ...this.exportConfig.context, branch: targetBranch?.uid }, - messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid }), - ); - throw new Error(messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid })); + const originalMessage = (error as Error)?.message ?? ''; + const errorMessage = + originalMessage || messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid }); + handleAndLogError(error, { ...this.exportConfig.context, branch: targetBranch?.uid }, errorMessage); + throw new Error(errorMessage); } } diff --git a/packages/contentstack-export/src/export/modules/assets.ts b/packages/contentstack-export/src/export/modules/assets.ts index 1362e8ff..a54a4b84 100644 --- a/packages/contentstack-export/src/export/modules/assets.ts +++ b/packages/contentstack-export/src/export/modules/assets.ts @@ -1,5 +1,5 @@ import map from 'lodash/map'; -import { getChalk } from '@contentstack/cli-utilities'; +import { cliux, getChalk } from '@contentstack/cli-utilities'; import chunk from 'lodash/chunk'; import first from 'lodash/first'; import merge from 'lodash/merge'; @@ -34,6 +34,7 @@ import { MODULE_NAMES, getOrgUid, } from '../../utils'; +import { handle } from '@oclif/core'; export default class ExportAssets extends BaseClass { private assetsRootPath: string; @@ -61,13 +62,27 @@ export default class ExportAssets extends BaseClass { if (linkedWorkspaces.length > 0) { const assetManagementUrl = this.exportConfig.region?.assetManagementUrl; if (!assetManagementUrl) { - this.completeProgress( - false, - 'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.', + handleAndLogError( + new Error( + 'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.', + ), + { + ...this.exportConfig.context, + message: + 'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.', + }, ); - throw new Error( + this.completeProgressWithMessage({ + moduleName: 'Asset Management 2.0', + customWarningMessage: + 'Asset Management 2.0 export was skipped: assetManagementUrl is not configured. AM 2.0 assets will not be exported.', + context: this.exportConfig.context, + }); + cliux.print( 'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.', + { color: 'yellow' }, ); + return; } log.debug( `Exporting with AM 2.0: ${assetManagementUrl} (linked_workspaces from exportConfig)`, diff --git a/packages/contentstack-export/src/utils/get-linked-workspaces.ts b/packages/contentstack-export/src/utils/get-linked-workspaces.ts index c9f17338..e5abca97 100644 --- a/packages/contentstack-export/src/utils/get-linked-workspaces.ts +++ b/packages/contentstack-export/src/utils/get-linked-workspaces.ts @@ -21,12 +21,14 @@ export async function getLinkedWorkspacesForBranch( log.debug('No linked_workspaces in branch settings', context); return []; } - log.info( - `Found ${linked.length} linked workspace(s) for branch ${branchName}`, - context, - ); + log.info(`Found ${linked.length} linked workspace(s) for branch ${branchName}`, context); return linked as LinkedWorkspace[]; } catch (error) { + const err = error as any; + if (err?.status === 412 || err?.errorCode === 412) { + log.warn('Branch settings not found, please check if the branches are enabled in your stack', context); + return []; + } handleAndLogError(error as Error, context as any, 'Failed to fetch branch settings'); return []; } diff --git a/packages/contentstack-import/src/utils/import-config-handler.ts b/packages/contentstack-import/src/utils/import-config-handler.ts index ea052eb7..29c00c1c 100644 --- a/packages/contentstack-import/src/utils/import-config-handler.ts +++ b/packages/contentstack-import/src/utils/import-config-handler.ts @@ -127,6 +127,7 @@ const setupConfig = async (importCmdFlags: any): Promise => { const spacesDir = path.join(config.contentDir, 'spaces'); const stackSettingsPath = path.join(config.contentDir, 'stack', 'settings.json'); + const stackJsonPath = path.join(config.contentDir, 'stack', 'stack.json'); if (existsSync(spacesDir) && existsSync(stackSettingsPath)) { try { @@ -135,22 +136,15 @@ const setupConfig = async (importCmdFlags: any): Promise => { config.assetManagementEnabled = true; config.assetManagementUrl = configHandler.get('region')?.assetManagementUrl; - const branchesJsonCandidates = [ - path.join(config.contentDir, 'branches.json'), - path.join(config.contentDir, '..', 'branches.json'), - ]; - for (const branchesJsonPath of branchesJsonCandidates) { - if (existsSync(branchesJsonPath)) { - try { - const branches = JSON.parse(readFileSync(branchesJsonPath, 'utf8')); - const apiKey = branches?.[0]?.stackHeaders?.api_key; - if (apiKey) { - config.source_stack = apiKey; - } - } catch { - // branches.json unreadable — URL mapping will be skipped + if (existsSync(stackJsonPath)) { + try { + const stackData = JSON.parse(readFileSync(stackJsonPath, 'utf8')); + const apiKey = stackData?.api_key || stackData?.stackHeaders?.api_key; + if (apiKey) { + config.source_stack = apiKey; } - break; + } catch { + // stack.json unreadable — source stack API key will not be set } } }