Skip to content

Commit cc5890e

Browse files
committed
merged changes
2 parents 6d4063f + 78c00d5 commit cc5890e

125 files changed

Lines changed: 5372 additions & 2170 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Cursor (optional)
2+
3+
**Cursor** users: start at **[AGENTS.md](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`**.
4+
5+
This folder only points contributors to **`AGENTS.md`** so editor-specific config does not duplicate the canonical docs.

.talismanrc

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
11
fileignoreconfig:
22
- filename: pnpm-lock.yaml
3-
checksum: 0efb2a2f86f58006abbacf7de17e0d498824c046d1d3cb58c6a64b7364e93ff5
4-
- filename: packages/contentstack-audit/src/modules/assets.ts
5-
checksum: 968dde65ca1e6526d3a16766c86d4d9976752ba8589c27facbbb4df7a8f1f963
3+
checksum: 11bf156488b12f529ab393fffa8e9d8741388ef2929263e89650eada87bfcf8a
4+
- filename: packages/contentstack-branches/README.md
5+
checksum: ad32bd365db7f085cc2ea133d69748954606131ec6157a272a3471aea60011c2
6+
- filename: packages/contentstack-branches/src/branch/diff-handler.ts
7+
checksum: 3cd4d26a2142cab7cbf2094c9251e028467d17d6a1ed6daf22f21975133805f1
8+
- filename: packages/contentstack-branches/src/commands/cm/branches/merge-status.ts
9+
checksum: 6e5b959ddcc5ff68e03c066ea185fcf6c6e57b1819069730340af35aad8a93a8
10+
- filename: packages/contentstack-branches/src/utils/create-branch.ts
11+
checksum: d0613295ee26f7a77d026e40db0a4ab726fabd0a74965f729f1a66d1ef14768f
12+
- filename: packages/contentstack-branches/src/branch/merge-handler.ts
13+
checksum: 4fd8dba9b723733530b9ba12e81e1d3e5d60b73ac4c082defb10593f257bb133
14+
- filename: packages/contentstack-export/src/export/modules/environments.ts
15+
checksum: a92c5de7ed8e80f08f911727973a66e0416b4a52265c275d1d25c3095f912811
16+
- filename: packages/contentstack-import/src/utils/backup-handler.ts
17+
checksum: 9a892b5c4b5aac230fb5969e7f34afdac0b6f96208e64bf9d1195468c935c66c
18+
- filename: packages/contentstack-import/test/unit/utils/backup-handler.test.ts
19+
checksum: 69860727e9b3099d8e1e95db2af17fc8b161684f675477981d27877cd8e1b3bb
20+
- filename: pnpm-lock.yaml
21+
checksum: 11bf156488b12f529ab393fffa8e9d8741388ef2929263e89650eada87bfcf8a
22+
- filename: packages/contentstack-export/test/unit/export/modules/marketplace-apps.test.ts
23+
checksum: 299b8f60cce1f64be7c20786d6a7c9c370474b97b06d1846114a76a70ec20cf7
24+
- filename: packages/contentstack-export/test/unit/export/module-exporter.test.ts
25+
checksum: 67b70c93ed679ccb2c61d0c277380676e33c91da8a423f948e81937e5d1d9479
26+
- filename: packages/contentstack-asset-management/src/import/assets.ts
27+
checksum: ed6af5d798282808c09643e1dcd1eaede89ce2b09bd0425998af64849b4f3f61
28+
- filename: skills/code-review/SKILL.md
29+
checksum: 29673e16f6b41fcec7fa236912e7f72b920ed4a3d9a66a89308b4a058b247f3e
30+
- filename: skills/contentstack-cli/SKILL.md
31+
checksum: 36762d43bbacedd0b344f9d4f1179a88e3dbc7e2467341ba42198dcd1bf9e40c
32+
- filename: skills/testing/SKILL.md
33+
checksum: ee1c82f1bb51860cb26fb9f112a53df0127e316fcb22a094034024741251fa3c
634
version: '1.0'

AGENTS.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Contentstack CLI plugins – Agent guide
2+
3+
**Universal entry point** for contributors and AI agents. Detailed conventions live in **`skills/*/SKILL.md`**.
4+
5+
## What this repo is
6+
7+
| Field | Detail |
8+
| --- | --- |
9+
| **Name:** | Contentstack CLI plugins (pnpm monorepo; root package name `csdx`) |
10+
| **Purpose:** | OCLIF plugins that extend the Contentstack CLI (import/export, clone, migration, seed, audit, variants, etc.). |
11+
| **Out of scope (if any):** | The **core** CLI aggregation lives in the separate `cli` monorepo; this repo ships plugin packages only. |
12+
13+
## Tech stack (at a glance)
14+
15+
| Area | Details |
16+
| --- | --- |
17+
| **Language** | TypeScript / JavaScript, Node **>= 18** (`engines` in root `package.json`) |
18+
| **Build** | pnpm workspaces (`packages/*`); per package: `tsc`, OCLIF manifest/readme where applicable → `lib/` |
19+
| **Tests** | Mocha + Chai; layouts under `packages/*/test/` (see [skills/testing/SKILL.md](skills/testing/SKILL.md)) |
20+
| **Lint / coverage** | ESLint in packages that define `lint` scripts; nyc where configured |
21+
| **Other** | OCLIF v4, Husky |
22+
23+
## Commands (quick reference)
24+
25+
| Command type | Command |
26+
| --- | --- |
27+
| **Build** | `pnpm build` |
28+
| **Test** | `pnpm test` |
29+
| **Lint** | `pnpm run lint` in a package that defines `lint` (no root aggregate lint script) |
30+
31+
CI: [.github/workflows/unit-test.yml](.github/workflows/unit-test.yml) and other workflows under [.github/workflows/](.github/workflows/).
32+
33+
## Where the documentation lives: skills
34+
35+
| Skill | Path | What it covers |
36+
| --- | --- | --- |
37+
| Development workflow | [skills/dev-workflow/SKILL.md](skills/dev-workflow/SKILL.md) | pnpm commands, CI, TDD expectations, PR checklist |
38+
| Contentstack CLI | [skills/contentstack-cli/SKILL.md](skills/contentstack-cli/SKILL.md) | Plugin commands, OCLIF, Contentstack APIs |
39+
| Framework | [skills/framework/SKILL.md](skills/framework/SKILL.md) | Utilities, config, logging, errors |
40+
| Testing | [skills/testing/SKILL.md](skills/testing/SKILL.md) | Mocha/Chai, coverage, mocks |
41+
| Code review | [skills/code-review/SKILL.md](skills/code-review/SKILL.md) | PR review for this monorepo |
42+
43+
## Using Cursor (optional)
44+
45+
If you use **Cursor**, [.cursor/rules/README.md](.cursor/rules/README.md) only points to **`AGENTS.md`**—same docs as everyone else.

packages/contentstack-asset-management/src/export/asset-types.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ export default class ExportAssetTypes extends AssetManagementExportAdapter {
1313

1414
async start(spaceUid: string): Promise<void> {
1515
await this.init();
16+
17+
log.debug('Starting shared asset types export process...', this.exportContext.context);
18+
1619
const assetTypesData = await this.getWorkspaceAssetTypes(spaceUid);
1720
const items = getArrayFromResponse(assetTypesData, 'asset_types');
1821
const dir = this.getAssetTypesDir();
19-
log.debug(
20-
items.length === 0
21-
? 'No asset types, wrote empty asset-types'
22-
: `Writing ${items.length} shared asset types`,
23-
this.exportContext.context,
24-
);
22+
if (items.length === 0) {
23+
log.info('No asset types to export, writing empty asset-types', this.exportContext.context);
24+
} else {
25+
log.debug(`Writing ${items.length} shared asset types`, this.exportContext.context);
26+
}
2527
await this.writeItemsToChunkedJson(dir, 'asset-types.json', 'asset_types', ['uid', 'title', 'category', 'file_extension'], items);
2628
this.tick(true, PROCESS_NAMES.AM_ASSET_TYPES, null);
2729
}

packages/contentstack-asset-management/src/export/assets.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { AssetManagementAPIConfig, LinkedWorkspace } from '../types/asset-m
77
import type { ExportContext } from '../types/export-types';
88
import { AssetManagementExportAdapter } from './base';
99
import { getAssetItems, writeStreamToFile } from '../utils/export-helpers';
10+
import { runInBatches } from '../utils/concurrent-batch';
1011
import { PROCESS_NAMES, PROCESS_STATUS } from '../constants/index';
1112

1213
export default class ExportAssets extends AssetManagementExportAdapter {
@@ -16,8 +17,14 @@ export default class ExportAssets extends AssetManagementExportAdapter {
1617

1718
async start(workspace: LinkedWorkspace, spaceDir: string): Promise<void> {
1819
await this.init();
20+
21+
log.debug(`Starting assets export for space ${workspace.space_uid}`, this.exportContext.context);
22+
log.info(`Exporting asset folders, metadata, and files for space ${workspace.space_uid}`, this.exportContext.context);
23+
1924
const assetsDir = pResolve(spaceDir, 'assets');
2025
await mkdir(assetsDir, { recursive: true });
26+
log.debug(`Assets directory ready: ${assetsDir}`, this.exportContext.context);
27+
2128
log.debug(`Fetching folders and assets for space ${workspace.space_uid}`, this.exportContext.context);
2229

2330
const [folders, assetsData] = await Promise.all([
@@ -43,37 +50,61 @@ export default class ExportAssets extends AssetManagementExportAdapter {
4350
['uid', 'url', 'filename', 'file_name', 'parent_uid'],
4451
assetItems,
4552
);
53+
log.debug(
54+
`Finished writing chunked assets metadata (${assetItems.length} item(s)) under ${assetsDir}`,
55+
this.exportContext.context,
56+
);
57+
log.info(
58+
assetItems.length === 0
59+
? `Wrote empty asset metadata for space ${workspace.space_uid}`
60+
: `Wrote ${assetItems.length} asset metadata record(s) for space ${workspace.space_uid}`,
61+
this.exportContext.context,
62+
);
4663
this.tick(true, `assets: ${workspace.space_uid} (${assetItems.length})`, null);
4764

65+
log.debug(`Starting binary downloads for space ${workspace.space_uid}`, this.exportContext.context);
4866
await this.downloadWorkspaceAssets(assetsData, assetsDir, workspace.space_uid);
4967
}
5068

51-
private async downloadWorkspaceAssets(
52-
assetsData: unknown,
53-
assetsDir: string,
54-
spaceUid: string,
55-
): Promise<void> {
69+
private async downloadWorkspaceAssets(assetsData: unknown, assetsDir: string, spaceUid: string): Promise<void> {
5670
const items = getAssetItems(assetsData);
5771
if (items.length === 0) {
72+
log.info(`No asset files to download for space ${spaceUid}`, this.exportContext.context);
5873
log.debug('No assets to download', this.exportContext.context);
5974
return;
6075
}
6176

6277
this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_DOWNLOADS].DOWNLOADING);
78+
log.info(`Downloading asset files for space ${spaceUid} (${items.length} in metadata)`, this.exportContext.context);
6379
log.debug(`Downloading ${items.length} asset file(s) for space ${spaceUid}...`, this.exportContext.context);
6480
const filesDir = pResolve(assetsDir, 'files');
6581
await mkdir(filesDir, { recursive: true });
82+
log.debug(`Asset files directory ready: ${filesDir}`, this.exportContext.context);
6683

6784
const securedAssets = this.exportContext.securedAssets ?? false;
6885
const authtoken = securedAssets ? configHandler.get('authtoken') : null;
86+
log.debug(
87+
`Asset downloads: securedAssets=${securedAssets}, concurrency=${this.downloadAssetsBatchConcurrency}`,
88+
this.exportContext.context,
89+
);
6990
let lastError: string | null = null;
7091
let allSuccess = true;
92+
let downloadOk = 0;
93+
let downloadFail = 0;
7194

72-
for (const asset of items) {
95+
const validItems = items.filter((asset) => Boolean(asset.url && (asset.uid ?? asset._uid)));
96+
const skipped = items.length - validItems.length;
97+
if (skipped > 0) {
98+
log.debug(
99+
`Skipping ${skipped} asset row(s) without url or uid (${validItems.length} file download(s) scheduled)`,
100+
this.exportContext.context,
101+
);
102+
}
103+
await runInBatches(validItems, this.downloadAssetsBatchConcurrency, async (asset) => {
73104
const uid = asset.uid ?? asset._uid;
74105
const url = asset.url;
75106
const filename = asset.filename ?? asset.file_name ?? 'asset';
76-
if (!url || !uid) continue;
107+
if (!url || !uid) return;
77108
try {
78109
const separator = url.includes('?') ? '&' : '?';
79110
const downloadUrl = securedAssets && authtoken ? `${url}${separator}authtoken=${authtoken}` : url;
@@ -86,15 +117,26 @@ export default class ExportAssets extends AssetManagementExportAdapter {
86117
await mkdir(assetFolderPath, { recursive: true });
87118
const filePath = pResolve(assetFolderPath, filename);
88119
await writeStreamToFile(nodeStream, filePath);
89-
log.debug(`Downloaded asset ${uid}`, this.exportContext.context);
120+
downloadOk += 1;
121+
log.debug(`Downloaded asset ${uid}${filePath}`, this.exportContext.context);
90122
} catch (e) {
91123
allSuccess = false;
124+
downloadFail += 1;
92125
lastError = (e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_DOWNLOADS].FAILED;
93126
log.debug(`Failed to download asset ${uid}: ${e}`, this.exportContext.context);
94127
}
95-
}
128+
});
96129

97130
this.tick(allSuccess, `downloads: ${spaceUid}`, lastError);
98-
log.debug('Asset downloads completed', this.exportContext.context);
131+
log.info(
132+
allSuccess
133+
? `Finished downloading ${downloadOk} asset file(s) for space ${spaceUid}`
134+
: `Asset downloads for space ${spaceUid} completed with errors: ${downloadOk} succeeded, ${downloadFail} failed`,
135+
this.exportContext.context,
136+
);
137+
log.debug(
138+
`Asset downloads finished for space ${spaceUid}: ok=${downloadOk}, failed=${downloadFail}, allSuccess=${allSuccess}`,
139+
this.exportContext.context,
140+
);
99141
}
100142
}

packages/contentstack-asset-management/src/export/base.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { FsUtility, log, CLIProgressManager, configHandler } from '@contentstack
55
import type { AssetManagementAPIConfig } from '../types/asset-management-api';
66
import type { ExportContext } from '../types/export-types';
77
import { AssetManagementAdapter } from '../utils/asset-management-api-adapter';
8-
import { AM_MAIN_PROCESS_NAME, FALLBACK_AM_CHUNK_FILE_SIZE_MB } from '../constants/index';
8+
import { AM_MAIN_PROCESS_NAME, FALLBACK_AM_API_CONCURRENCY, FALLBACK_AM_CHUNK_FILE_SIZE_MB } from '../constants/index';
99

1010
export type { ExportContext };
1111

@@ -63,6 +63,16 @@ export class AssetManagementExportAdapter extends AssetManagementAdapter {
6363
return this.exportContext.spacesRootPath;
6464
}
6565

66+
/** Parallel AM export limit for bootstrap and default batch operations. */
67+
protected get apiConcurrency(): number {
68+
return this.exportContext.apiConcurrency ?? FALLBACK_AM_API_CONCURRENCY;
69+
}
70+
71+
/** Asset download batch size; falls back to {@link apiConcurrency}. */
72+
protected get downloadAssetsBatchConcurrency(): number {
73+
return this.exportContext.downloadAssetsConcurrency ?? this.apiConcurrency;
74+
}
75+
6676
protected getAssetTypesDir(): string {
6777
return pResolve(this.exportContext.spacesRootPath, 'asset_types');
6878
}

packages/contentstack-asset-management/src/export/fields.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ export default class ExportFields extends AssetManagementExportAdapter {
1313

1414
async start(spaceUid: string): Promise<void> {
1515
await this.init();
16+
17+
log.debug('Starting shared fields export process...', this.exportContext.context);
18+
1619
const fieldsData = await this.getWorkspaceFields(spaceUid);
1720
const items = getArrayFromResponse(fieldsData, 'fields');
1821
const dir = this.getFieldsDir();
19-
log.debug(
20-
items.length === 0 ? 'No field items, wrote empty fields' : `Writing ${items.length} shared fields`,
21-
this.exportContext.context,
22-
);
22+
if (items.length === 0) {
23+
log.info('No field items to export, writing empty fields', this.exportContext.context);
24+
} else {
25+
log.debug(`Writing ${items.length} shared fields`, this.exportContext.context);
26+
}
2327
await this.writeItemsToChunkedJson(dir, 'fields.json', 'fields', ['uid', 'title', 'display_type'], items);
2428
this.tick(true, PROCESS_NAMES.AM_FIELDS, null);
2529
}

packages/contentstack-asset-management/src/export/spaces.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { resolve as pResolve } from 'node:path';
22
import { mkdir } from 'node:fs/promises';
3-
import { log, CLIProgressManager, configHandler } from '@contentstack/cli-utilities';
3+
import { log, CLIProgressManager, configHandler, handleAndLogError } from '@contentstack/cli-utilities';
44

55
import type { AssetManagementExportOptions, AssetManagementAPIConfig } from '../types/asset-management-api';
66
import type { ExportContext } from '../types/export-types';
@@ -46,6 +46,8 @@ export class ExportSpaces {
4646
return;
4747
}
4848

49+
log.debug('Starting Asset Management export process...', context);
50+
log.info('Started Asset Management export', context);
4951
log.debug(`Exporting Asset Management 2.0 (${linkedWorkspaces.length} space(s))`, context);
5052
log.debug(`Spaces: ${linkedWorkspaces.map((ws) => ws.space_uid).join(', ')}`, context);
5153

@@ -70,6 +72,8 @@ export class ExportSpaces {
7072
context,
7173
securedAssets,
7274
chunkFileSizeMb,
75+
apiConcurrency: this.options.apiConcurrency,
76+
downloadAssetsConcurrency: this.options.downloadAssetsConcurrency,
7377
};
7478

7579
const sharedFieldsDir = pResolve(spacesRootPath, 'fields');
@@ -81,11 +85,9 @@ export class ExportSpaces {
8185
try {
8286
const exportAssetTypes = new ExportAssetTypes(apiConfig, exportContext);
8387
exportAssetTypes.setParentProgressManager(progress);
84-
await exportAssetTypes.start(firstSpaceUid);
85-
8688
const exportFields = new ExportFields(apiConfig, exportContext);
8789
exportFields.setParentProgressManager(progress);
88-
await exportFields.start(firstSpaceUid);
90+
await Promise.all([exportAssetTypes.start(firstSpaceUid), exportFields.start(firstSpaceUid)]);
8991

9092
for (const ws of linkedWorkspaces) {
9193
progress.updateStatus(`Exporting space: ${ws.space_uid}...`, AM_MAIN_PROCESS_NAME);
@@ -109,9 +111,11 @@ export class ExportSpaces {
109111
}
110112

111113
progress.completeProcess(AM_MAIN_PROCESS_NAME, true);
114+
log.info('Asset Management export completed successfully', context);
112115
log.debug('Asset Management 2.0 export completed', context);
113116
} catch (err) {
114117
progress.completeProcess(AM_MAIN_PROCESS_NAME, false);
118+
handleAndLogError(err, { ...(context as Record<string, unknown>) }, 'Asset Management export failed');
115119
throw err;
116120
}
117121
}

packages/contentstack-asset-management/src/export/workspaces.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export default class ExportWorkspace extends AssetManagementExportAdapter {
1515

1616
async start(workspace: LinkedWorkspace, spaceDir: string, branchName: string): Promise<void> {
1717
await this.init();
18+
19+
log.debug(`Starting export for AM space ${workspace.space_uid}`, this.exportContext.context);
20+
1821
const spaceResponse = await this.getSpace(workspace.space_uid);
1922
const space = spaceResponse.space;
2023
await mkdir(spaceDir, { recursive: true });
@@ -25,7 +28,13 @@ export default class ExportWorkspace extends AssetManagementExportAdapter {
2528
is_default: workspace.is_default,
2629
branch: branchName || 'main',
2730
};
28-
await writeFile(pResolve(spaceDir, 'metadata.json'), JSON.stringify(metadata, null, 2));
31+
const metadataPath = pResolve(spaceDir, 'metadata.json');
32+
try {
33+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
34+
} catch (e) {
35+
log.warn(`Could not write ${metadataPath}: ${e}`, this.exportContext.context);
36+
throw e;
37+
}
2938
this.tick(true, `space: ${workspace.space_uid}`, null);
3039
log.debug(`Space metadata written for ${workspace.space_uid}`, this.exportContext.context);
3140

0 commit comments

Comments
 (0)