Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1cf97ff
Updated branches merge revamp to handle poling timeouts
netrajpatel Mar 27, 2026
854f828
Lint fixes
netrajpatel Mar 27, 2026
a973409
Merge branch 'development' into feat/dx-5584
netrajpatel Mar 31, 2026
178bbf5
feat: Add AM2.0 support in import setup command
Apr 1, 2026
3161b7b
Reverted generate script command
netrajpatel Apr 2, 2026
d4725bb
Merge branch 'development' into feat/dx-5584
netrajpatel Apr 2, 2026
d96e20b
Merge branch 'feat/AM2.0' into feat/DX-5418
Apr 3, 2026
f498bd2
chore: optimise code
Apr 3, 2026
0affac5
Merge branch 'development' into feat/dx-5584
netrajpatel Apr 6, 2026
55a05d1
chore: add test cases in Clone for AM
Apr 7, 2026
2df81f5
Merge pull request #80 from contentstack/chore/DX-5419
naman-contentstack Apr 8, 2026
057c3e0
Bumped versions
netrajpatel Apr 8, 2026
90e92c0
Merge branch 'feat/dx-5584' of github.com:contentstack/cli-plugins in…
netrajpatel Apr 8, 2026
95ecf79
Merge branch 'development' into feat/dx-5584
netrajpatel Apr 17, 2026
991d6c0
Snyk fixes
netrajpatel Apr 17, 2026
3fe416a
Merge pull request #58 from contentstack/feat/dx-5584
netrajpatel Apr 17, 2026
196e55b
fix: Dependency updated
cs-raj Apr 20, 2026
f03606f
updated lockfile
cs-raj Apr 20, 2026
ae59719
chore: dependency update
cs-raj Apr 20, 2026
030e8cc
Merge pull request #97 from contentstack/fix/DX-6181
cs-raj Apr 20, 2026
88d64b4
chore: version bump
cs-raj Apr 20, 2026
be9b283
chore: version update
Apr 21, 2026
97e4410
chore: version update
Apr 21, 2026
fe96837
Merge pull request #99 from contentstack/chore/Version
naman-contentstack Apr 21, 2026
c5ef996
Merge pull request #100 from contentstack/development
naman-contentstack Apr 21, 2026
a7d3c56
Merge pull request #103 from contentstack/main
naman-contentstack Apr 21, 2026
00cb29d
Merge branch 'development' into fix/back-merge
Apr 21, 2026
dc45914
Merge branch 'feat/AM2.0' into feat/DX-5418
Apr 22, 2026
9e4d9b8
chore: fix test cases
Apr 22, 2026
3a87a46
Merge pull request #105 from contentstack/fix/back-merge
naman-contentstack Apr 22, 2026
592569b
Merge branch 'v2-dev' into feat/DX-5418
Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ fileignoreconfig:
checksum: 9a892b5c4b5aac230fb5969e7f34afdac0b6f96208e64bf9d1195468c935c66c
- filename: packages/contentstack-import/test/unit/utils/backup-handler.test.ts
checksum: 69860727e9b3099d8e1e95db2af17fc8b161684f675477981d27877cd8e1b3bb
- filename: pnpm-lock.yaml
checksum: 794f45eb36d5f0639963694dc6d5e0c6a789584e9a7a3e54bd0d531275c29857
- filename: packages/contentstack-export/test/unit/export/module-exporter.test.ts
checksum: 67b70c93ed679ccb2c61d0c277380676e33c91da8a423f948e81937e5d1d9479
- filename: packages/contentstack-export/test/unit/export/modules/marketplace-apps.test.ts
Expand All @@ -17,4 +15,16 @@ fileignoreconfig:
checksum: 29673e16f6b41fcec7fa236912e7f72b920ed4a3d9a66a89308b4a058b247f3e
- filename: skills/testing/SKILL.md
checksum: ee1c82f1bb51860cb26fb9f112a53df0127e316fcb22a094034024741251fa3c
- filename: pnpm-lock.yaml
checksum: 4861082320d7011b7adb96b6ca07f953f69e391d046a392e8f2b55cdd77b684f
- filename: packages/contentstack-branches/src/utils/create-branch.ts
checksum: d0613295ee26f7a77d026e40db0a4ab726fabd0a74965f729f1a66d1ef14768f
- 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/branch/merge-handler.ts
checksum: 4fd8dba9b723733530b9ba12e81e1d3e5d60b73ac4c082defb10593f257bb133
version: '1.0'
17 changes: 17 additions & 0 deletions packages/contentstack-asset-management/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export const FALLBACK_ASSET_TYPES_IMPORT_INVALID_KEYS = [

/** @deprecated Use FALLBACK_AM_CHUNK_FILE_SIZE_MB */
export const CHUNK_FILE_SIZE_MB = FALLBACK_AM_CHUNK_FILE_SIZE_MB;
/**
* Mapper output paths — must stay aligned with contentstack-import `PATH_CONSTANTS`
* (`mapper` / `assets` / uid, url, space-uid file names).
*/
export const IMPORT_ASSETS_MAPPER_DIR_SEGMENTS = ['mapper', 'assets'] as const;
export const IMPORT_ASSETS_MAPPER_FILES = {
UID_MAPPING: 'uid-mapping.json',
URL_MAPPING: 'url-mapping.json',
SPACE_UID_MAPPING: 'space-uid-mapping.json',
DUPLICATE_ASSETS: 'duplicate-assets.json',
} as const;

/**
* Main process name for Asset Management 2.0 export (single progress bar).
Expand All @@ -49,6 +60,8 @@ export const PROCESS_NAMES = {
AM_IMPORT_ASSET_TYPES: 'Import asset types',
AM_IMPORT_FOLDERS: 'Import folders',
AM_IMPORT_ASSETS: 'Import assets',
/** Import-setup (CLI): generate uid/url/space mappers from AM export before full import. */
AM_IMPORT_SETUP_ASSET_MAPPERS: 'Import setup asset mappers',
} as const;

/**
Expand Down Expand Up @@ -95,4 +108,8 @@ export const PROCESS_STATUS = {
IMPORTING: 'Importing assets...',
FAILED: 'Failed to import assets.',
},
[PROCESS_NAMES.AM_IMPORT_SETUP_ASSET_MAPPERS]: {
GENERATING: 'Generating asset mappers...',
FAILED: 'Failed to generate asset mappers.',
},
} as const;
23 changes: 23 additions & 0 deletions packages/contentstack-asset-management/src/import-setup/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { CLIProgressManager } from '@contentstack/cli-utilities';

import type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';

/**
* Base for CLI import-setup flows that prepare AM exports (mappers, metadata) before full import.
* Mirrors ImportSpaces-style `setParentProgressManager`; callers log via `@contentstack/cli-utilities` `log` + `params.context`.
*/
export abstract class AssetManagementImportSetupAdapter {
private parentProgressManager: CLIProgressManager | null = null;

protected constructor(protected readonly params: RunAssetMapperImportSetupParams) {}

public setParentProgressManager(parent: CLIProgressManager): void {
this.parentProgressManager = parent;
}

protected resolveParentProgress(): CLIProgressManager | null {
return this.parentProgressManager;
}

abstract start(): Promise<AssetMapperImportSetupResult>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { readdirSync, statSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';

import { formatError, log } from '@contentstack/cli-utilities';

import { IMPORT_ASSETS_MAPPER_FILES, PROCESS_NAMES, PROCESS_STATUS } from '../constants/index';
import type { AssetManagementAPIConfig, ImportContext } from '../types/asset-management-api';
import type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';
import ImportAssets from '../import/assets';
import { AssetManagementAdapter } from '../utils/asset-management-api-adapter';
import { AssetManagementImportSetupAdapter } from './base';

const PROCESS = PROCESS_NAMES.AM_IMPORT_SETUP_ASSET_MAPPERS;

/**
* Builds identity uid/url and space-uid mapper files from an Asset Management export layout
* for spaces that already exist in the target org (reuse path).
*/
export default class ImportSetupAssetMappers extends AssetManagementImportSetupAdapter {
constructor(params: RunAssetMapperImportSetupParams) {
super(params);
}

private async fetchExistingSpaceUidsInOrg(apiConfig: AssetManagementAPIConfig): Promise<Set<string>> {
const adapter = new AssetManagementAdapter(apiConfig);
await adapter.init();
const { spaces } = await adapter.listSpaces();
const uids = new Set<string>();
for (const s of spaces) {
if (s.uid) {
uids.add(s.uid);
}
}
return uids;
}

private listExportedSpaceDirectories(spacesRootPath: string): { spaceDirs: string[]; readFailed: boolean } {
try {
const spaceDirs = readdirSync(spacesRootPath).filter((entry) => {
try {
return statSync(join(spacesRootPath, entry)).isDirectory() && entry.startsWith('am');
} catch {
return false;
}
});
return { spaceDirs, readFailed: false };
} catch {
log.info(`Could not read Asset Management spaces directory: ${spacesRootPath}`, this.params.context);
return { spaceDirs: [], readFailed: true };
}
}

async start(): Promise<AssetMapperImportSetupResult> {
const p = this.params;
const {
contentDir,
mapperBaseDir,
assetManagementUrl,
org_uid,
source_stack,
apiKey,
host,
context,
} = p;

const apiConcurrencyResolved = p.apiConcurrency ?? p.fetchConcurrency;

if (!assetManagementUrl) {
log.info(
'AM 2.0 export detected but assetManagementUrl is not configured in the region settings. Skipping AM 2.0 asset mapper setup.',
context,
);
return { kind: 'skipped', reason: 'missing_asset_management_url' };
}
if (!org_uid) {
log.error('Cannot run Asset Management import-setup: organization UID is missing.', context);
return { kind: 'skipped', reason: 'missing_organization_uid' };
}

const parentProgressManager = this.resolveParentProgress();

const spacesDirSegment = p.spacesDirName ?? 'spaces';
const spacesRootPath = resolve(contentDir, spacesDirSegment);
const mapperRoot = p.mapperRootDir ?? 'mapper';
const mapperAssetsMod = p.mapperAssetsModuleDir ?? 'assets';
const mapperDirPath = join(mapperBaseDir, mapperRoot, mapperAssetsMod);
const uidFile = p.mapperUidFileName ?? IMPORT_ASSETS_MAPPER_FILES.UID_MAPPING;
const urlFile = p.mapperUrlFileName ?? IMPORT_ASSETS_MAPPER_FILES.URL_MAPPING;
const spaceUidFile = p.mapperSpaceUidFileName ?? IMPORT_ASSETS_MAPPER_FILES.SPACE_UID_MAPPING;
const duplicateAssetMapperPath = join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.DUPLICATE_ASSETS);

const apiConfig: AssetManagementAPIConfig = {
baseURL: assetManagementUrl,
headers: { organization_uid: org_uid },
context,
};

const importContext: ImportContext = {
spacesRootPath,
sourceApiKey: source_stack,
apiKey,
host,
org_uid,
context,
apiConcurrency: apiConcurrencyResolved,
spacesDirName: p.spacesDirName,
fieldsDir: p.fieldsDir,
assetTypesDir: p.assetTypesDir,
fieldsFileName: p.fieldsFileName,
assetTypesFileName: p.assetTypesFileName,
foldersFileName: p.foldersFileName,
assetsFileName: p.assetsFileName,
fieldsImportInvalidKeys: p.fieldsImportInvalidKeys,
assetTypesImportInvalidKeys: p.assetTypesImportInvalidKeys,
uploadAssetsConcurrency: p.uploadAssetsConcurrency,
importFoldersConcurrency: p.importFoldersConcurrency,
mapperRootDir: mapperRoot,
mapperAssetsModuleDir: mapperAssetsMod,
mapperUidFileName: uidFile,
mapperUrlFileName: urlFile,
mapperSpaceUidFileName: spaceUidFile,
};

try {
if (parentProgressManager) {
parentProgressManager.addProcess(PROCESS, 1);
parentProgressManager
.startProcess(PROCESS)
.updateStatus(PROCESS_STATUS[PROCESS].GENERATING, PROCESS);
}

const existingSpaceUids = await this.fetchExistingSpaceUidsInOrg(apiConfig);

const { spaceDirs, readFailed } = this.listExportedSpaceDirectories(spacesRootPath);
if (spaceDirs.length === 0 && !readFailed) {
log.info(
`No Asset Management space directories (am*) found under ${spacesDirSegment}/.`,
context,
);
}

const allUidMap: Record<string, string> = {};
const allUrlMap: Record<string, string> = {};
const spaceUidMap: Record<string, string> = {};

const assetsImporter = new ImportAssets(apiConfig, importContext);

for (const spaceUid of spaceDirs) {
const spaceDir = join(spacesRootPath, spaceUid);
if (existingSpaceUids.has(spaceUid)) {
const { uidMap, urlMap } = await assetsImporter.buildIdentityMappersFromExport(spaceDir);
Object.assign(allUidMap, uidMap);
Object.assign(allUrlMap, urlMap);
spaceUidMap[spaceUid] = spaceUid;
parentProgressManager?.tick(true, `Asset Management space reused: ${spaceUid}`, null, PROCESS);
log.info(
`Asset Management space "${spaceUid}" exists in org; identity asset mappers merged from export.`,
context,
);
} else {
log.info(
`Asset Management space "${spaceUid}" is not in the target org yet. Import assets first, then re-run import-setup to refresh mappers after upload.`,
context,
);
}
}

await mkdir(mapperDirPath, { recursive: true });

await writeFile(join(mapperDirPath, uidFile), JSON.stringify(allUidMap), 'utf8');
await writeFile(join(mapperDirPath, urlFile), JSON.stringify(allUrlMap), 'utf8');
await writeFile(join(mapperDirPath, spaceUidFile), JSON.stringify(spaceUidMap), 'utf8');

await writeFile(duplicateAssetMapperPath, JSON.stringify({}), 'utf8');

parentProgressManager?.completeProcess(PROCESS, true);
log.success(
'The required Asset Management setup files for assets have been generated successfully.',
context,
);

return { kind: 'success' };
} catch (error) {
parentProgressManager?.completeProcess(PROCESS, false);
log.error(`Error occurred while generating Asset Management asset mappers: ${formatError(error)}.`, context);
return {
kind: 'error',
errorMessage: (error as Error)?.message || 'Asset Management asset mapper generation failed',
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { AssetManagementImportSetupAdapter } from './base';
export { default as ImportSetupAssetMappers } from './import-setup-asset-mappers';
export type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';
1 change: 1 addition & 0 deletions packages/contentstack-asset-management/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './types';
export * from './utils';
export * from './export';
export * from './import';
export * from './import-setup';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Values derived from an on-disk export layout for Asset Management–backed stacks.
* Used by `contentstack-import` and `contentstack-import-setup` config handlers.
*/
export type AssetManagementExportFlags = {
assetManagementEnabled: boolean;
assetManagementUrl?: string;
/** Source stack API key from `branches.json`, when present — used for URL reconstruction. */
source_stack?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export type RunAssetMapperImportSetupParams = {
contentDir: string;
/** Parent of the assets mapper directory (typically import-setup `backupDir`). */
mapperBaseDir: string;
assetManagementUrl?: string;
org_uid?: string;
source_stack?: string;
apiKey: string;
host: string;
context: Record<string, unknown>;
/**
* Max parallel AM API calls for list/read paths.
* Takes precedence over {@link fetchConcurrency}.
*/
apiConcurrency?: number;
/**
* @deprecated Use {@link apiConcurrency}.
*/
fetchConcurrency?: number;
/** Relative dir under content dir for AM export root (default `spaces`). */
spacesDirName?: string;
fieldsDir?: string;
assetTypesDir?: string;
fieldsFileName?: string;
assetTypesFileName?: string;
foldersFileName?: string;
assetsFileName?: string;
fieldsImportInvalidKeys?: string[];
assetTypesImportInvalidKeys?: string[];
mapperRootDir?: string;
mapperAssetsModuleDir?: string;
mapperUidFileName?: string;
mapperUrlFileName?: string;
mapperSpaceUidFileName?: string;
uploadAssetsConcurrency?: number;
importFoldersConcurrency?: number;
};

export type AssetMapperImportSetupResult =
| { kind: 'skipped'; reason: 'missing_asset_management_url' | 'missing_organization_uid' }
| { kind: 'success' }
| { kind: 'error'; errorMessage: string };
2 changes: 2 additions & 0 deletions packages/contentstack-asset-management/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './asset-management-export-flags';
export * from './asset-management-api';
export * from './export-types';
export * from './import-setup-asset-mapper';
Loading