From 00388af32a7e88a6050a55b40aeada8b5e39014f Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 16:50:23 +0530 Subject: [PATCH 1/7] added org plan check (cherry picked from commit 41b73639afd0f62f7c27845bc9dbebaa678cafd9) --- .talismanrc | 4 + .../src/commands/config/set/region.ts | 18 +++ .../src/interfaces/index.ts | 2 + .../src/utils/region-handler.ts | 4 + .../src/feature-status/build-auth-headers.ts | 43 +++++++ .../feature-status/feature-status-handler.ts | 49 ++++++++ .../src/feature-status/feature-uids.ts | 6 + .../src/feature-status/index.ts | 5 + .../src/feature-status/resolve-auth-host.ts | 13 +++ .../src/feature-status/types.ts | 16 +++ packages/contentstack-utilities/src/index.ts | 2 + .../src/hooks/prerun/plan-guard.ts | 110 ++++++++++++++++++ .../contentstack/src/utils/context-handler.ts | 4 +- 13 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 packages/contentstack-utilities/src/feature-status/build-auth-headers.ts create mode 100644 packages/contentstack-utilities/src/feature-status/feature-status-handler.ts create mode 100644 packages/contentstack-utilities/src/feature-status/feature-uids.ts create mode 100644 packages/contentstack-utilities/src/feature-status/index.ts create mode 100644 packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts create mode 100644 packages/contentstack-utilities/src/feature-status/types.ts create mode 100644 packages/contentstack/src/hooks/prerun/plan-guard.ts diff --git a/.talismanrc b/.talismanrc index bedb52350d..169549a7cb 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,4 +1,8 @@ fileignoreconfig: - filename: pnpm-lock.yaml checksum: ba838353c1277083cb35b5f69cbbe00f0dc5d7d69e49b44c71c3a7a79e95ee73 + - filename: packages/contentstack-utilities/src/feature-status/build-auth-headers.ts + checksum: 9360dfbce41aa9c8a62889f4821c37fc222b04b8b1c552d1dfbcd8f2854d49e0 + - filename: packages/contentstack/src/hooks/prerun/plan-guard.ts + checksum: 74eeb5c98e9ae48bf6f2918e75a7cfca23915396ffdd177b8716acba35c287da version: '1.0' diff --git a/packages/contentstack-config/src/commands/config/set/region.ts b/packages/contentstack-config/src/commands/config/set/region.ts index edc1311bac..a0b8860813 100644 --- a/packages/contentstack-config/src/commands/config/set/region.ts +++ b/packages/contentstack-config/src/commands/config/set/region.ts @@ -51,6 +51,12 @@ export default class RegionSetCommand extends BaseCommand { + const host = resolveAuthHost(ctx); + const headers = buildAuthHeaders(ctx); + + const client = new HttpClient(); + client.baseUrl(host).headers(headers); + + const res = await client.get( + `/v1/feature-status?feature_uid=${encodeURIComponent(featureUid)}`, + ); + + if (res.status < 200 || res.status >= 300) { + throw new Error(`PLAN_CHECK: feature-status API returned ${res.status} for "${featureUid}".`); + } + + return res.data as FeatureStatus; +} + +export async function assertFeatureEnabled(featureUid: string, ctx?: FeatureCtx): Promise { + let status: FeatureStatus; + try { + status = await isFeatureEnabled(featureUid, ctx); + } catch (e) { + throw new Error( + `Could not verify your plan for "${featureUid}". ` + + `This command requires a confirmed plan status to run. ` + + `Please retry; if the problem persists contact support. (${(e as Error).message})`, + ); + } + + if (!status.is_part_of_plan) { + throw new Error( + `"${featureUid}" is not part of your current plan. Please upgrade your plan to use this feature.`, + ); + } + + if (status.status !== 'enabled') { + throw new Error( + `"${featureUid}" is not enabled for your plan (status: ${status.status}).`, + ); + } + + return status; +} diff --git a/packages/contentstack-utilities/src/feature-status/feature-uids.ts b/packages/contentstack-utilities/src/feature-status/feature-uids.ts new file mode 100644 index 0000000000..d333ffb81f --- /dev/null +++ b/packages/contentstack-utilities/src/feature-status/feature-uids.ts @@ -0,0 +1,6 @@ +export const FEATURE = { + ASSET_MANAGEMENT: 'amAssets', + // ASSET_SCANNING: 'asset_scanning', // uncomment when uid confirmed with platform team +} as const; + +export type FeatureUid = typeof FEATURE[keyof typeof FEATURE]; diff --git a/packages/contentstack-utilities/src/feature-status/index.ts b/packages/contentstack-utilities/src/feature-status/index.ts new file mode 100644 index 0000000000..97531ef9a3 --- /dev/null +++ b/packages/contentstack-utilities/src/feature-status/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export * from './feature-uids'; +export { resolveAuthHost } from './resolve-auth-host'; +export { buildAuthHeaders } from './build-auth-headers'; +export { isFeatureEnabled, assertFeatureEnabled } from './feature-status-handler'; diff --git a/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts b/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts new file mode 100644 index 0000000000..a86e4786ee --- /dev/null +++ b/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts @@ -0,0 +1,13 @@ +import configHandler from '../config-handler'; + +export function resolveAuthHost(ctx?: { region?: { authUrl?: string } }): string { + const region = (ctx?.region ?? configHandler.get('region')) as { authUrl?: string } | undefined; + const authUrl = region?.authUrl; + if (!authUrl) { + throw new Error( + 'PLAN_CHECK: Auth host is not configured for the current region. ' + + "Re-run `csdx config:set:region` to refresh region endpoints.", + ); + } + return String(authUrl).replace(/\/$/, ''); +} diff --git a/packages/contentstack-utilities/src/feature-status/types.ts b/packages/contentstack-utilities/src/feature-status/types.ts new file mode 100644 index 0000000000..136c3fee66 --- /dev/null +++ b/packages/contentstack-utilities/src/feature-status/types.ts @@ -0,0 +1,16 @@ +export interface FeatureStatus { + org_uid: string; + feature_key: string; + status: 'enabled' | 'disabled' | string; + limit: number; + max_limit: number; + is_part_of_plan: boolean; +} + +export interface FeatureCtx { + apiKey?: string; + orgUid?: string; + managementToken?: string; + authToken?: string; + region?: { authUrl?: string; name?: string; cma?: string }; +} diff --git a/packages/contentstack-utilities/src/index.ts b/packages/contentstack-utilities/src/index.ts index db62ccc212..66ed0e16a3 100644 --- a/packages/contentstack-utilities/src/index.ts +++ b/packages/contentstack-utilities/src/index.ts @@ -80,3 +80,5 @@ export { default as TablePrompt } from './inquirer-table-prompt'; export { Logger }; export { default as authenticationHandler } from './authentication-handler'; export {v2Logger as log, cliErrorHandler, handleAndLogError, getLogPath} from './logger/log' + +export * from './feature-status'; diff --git a/packages/contentstack/src/hooks/prerun/plan-guard.ts b/packages/contentstack/src/hooks/prerun/plan-guard.ts new file mode 100644 index 0000000000..fd835d22b6 --- /dev/null +++ b/packages/contentstack/src/hooks/prerun/plan-guard.ts @@ -0,0 +1,110 @@ +import { + configHandler, + assertFeatureEnabled, + FeatureCtx, + FeatureStatus, + log, + handleAndLogError, + cliux, +} from '@contentstack/cli-utilities'; + +function getArgvFlag(argv: string[], ...names: string[]): string | undefined { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + for (const name of names) { + if (arg === name && argv[i + 1] && !argv[i + 1].startsWith('-')) { + return argv[i + 1]; + } + if (arg.startsWith(`${name}=`)) { + return arg.split('=').slice(1).join('='); + } + } + } + return undefined; +} + +export default async function (opts: { Command?: { id?: string }; argv?: string[]; config?: any }): Promise { + const config = opts?.config ?? this.config; + const commandId = opts?.Command?.id; + const argv: string[] = opts?.argv ?? []; + + if (!commandId) return; + + const planProtectedCommands: Record = + config?.context?.plugin?.config?.planProtectedCommands ?? {}; + + const requiredFeatures: string[] = planProtectedCommands[commandId]; + if (!requiredFeatures || requiredFeatures.length === 0) return; + + // Resolve alias → management token + const aliasFromArgv = getArgvFlag(argv, '--alias', '-a'); + + // Clone uses different flag names — handle both alias forms + const cloneSourceAlias = + commandId === 'cm:stacks:clone' + ? getArgvFlag(argv, '--source-management-token-alias') ?? + getArgvFlag(argv, '--destination-management-token-alias') + : undefined; + + const alias = aliasFromArgv ?? cloneSourceAlias; + let managementToken: string | undefined; + let apiKeyFromAlias: string | undefined; + if (alias) { + const stored = configHandler.get(`tokens.${alias}`) as + | { token?: string; apiKey?: string; type?: string } + | undefined; + if (stored?.token && stored?.apiKey && stored?.type !== 'delivery') { + managementToken = stored.token; + apiKeyFromAlias = stored.apiKey; + } + } + + const authorisationType = configHandler.get('authorisationType') as string | undefined; + const oauthToken = configHandler.get('oauthAccessToken') as string | undefined; + const isOAuth = authorisationType === 'OAUTH' && !!oauthToken; + + const authtoken = configHandler.get('authtoken') as string | undefined; + const isBasic = authorisationType === 'BASIC' && !!authtoken; + + const apiKeyFromArgv = getArgvFlag(argv, '--stack-api-key', '-k'); + const orgUid = configHandler.get('oauthOrgUid') as string | undefined; + + const canCheckNow = + (!!managementToken && !!apiKeyFromAlias) || + isOAuth || + (isBasic && !!(apiKeyFromArgv || orgUid)); + + if (!canCheckNow) { + config.context.planCheckRequired = requiredFeatures; + log.debug( + `[plan-guard] Deferred plan check for: ${requiredFeatures.join(', ')} — credentials not resolvable at prerun`, + { module: 'plan-guard', commandId }, + ); + return; + } + + const region = configHandler.get('region') as { authUrl?: string; name?: string } | undefined; + const ctx: FeatureCtx = { + managementToken, + apiKey: apiKeyFromAlias ?? apiKeyFromArgv, + authToken: authtoken, + orgUid, + region, + }; + + const planStatus: Record = {}; + for (const featureUid of requiredFeatures) { + try { + const result = await assertFeatureEnabled(featureUid, ctx); + planStatus[featureUid] = result; + log.debug(`[plan-guard] Feature "${featureUid}" is enabled.`, { module: 'plan-guard', commandId }); + } catch (error) { + handleAndLogError(error, { module: 'plan-guard', commandId, featureUid }); + cliux.error((error as Error).message); + this.exit(1); + return; + } + } + + config.context.planStatus = planStatus; +} diff --git a/packages/contentstack/src/utils/context-handler.ts b/packages/contentstack/src/utils/context-handler.ts index bc4a0da678..f95798b731 100644 --- a/packages/contentstack/src/utils/context-handler.ts +++ b/packages/contentstack/src/utils/context-handler.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { configHandler, pathValidator, sanitizePath, generateShortUid } from '@contentstack/cli-utilities'; +import { configHandler, pathValidator, sanitizePath, generateShortUid, FeatureStatus } from '@contentstack/cli-utilities'; import { machineIdSync } from 'node-machine-id'; export default class CsdxContext { @@ -16,6 +16,8 @@ export default class CsdxContext { public flagWarningPrintState: any; public flags: any; public cliVersion: string; + public planStatus: Record = {}; + public planCheckRequired: string[] = []; constructor(cliOpts: any, cliConfig: any) { const analyticsInfo = []; From 927d385d2e2060ba5f9e6c7d43d9b941672004e1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 1 Jul 2026 11:21:03 +0530 Subject: [PATCH 2/7] fixed org plan check (cherry picked from commit 6db9d0d1547fc2fcb5481f8da116235265830437) --- packages/contentstack-command/src/index.ts | 2 +- .../src/feature-status/feature-uids.ts | 4 +- .../src/hooks/init/context-init.ts | 7 ++- .../src/hooks/prerun/plan-guard.ts | 56 ++++++------------- packages/contentstack/tsconfig.json | 2 +- 5 files changed, 28 insertions(+), 43 deletions(-) diff --git a/packages/contentstack-command/src/index.ts b/packages/contentstack-command/src/index.ts index 35d6cc6181..afc22781ad 100644 --- a/packages/contentstack-command/src/index.ts +++ b/packages/contentstack-command/src/index.ts @@ -14,7 +14,7 @@ abstract class ContentstackCommand extends Command { get context() { // @ts-ignore - return this.config.context || {}; + return this.config.context || this.config.options?.context || {}; } get email() { diff --git a/packages/contentstack-utilities/src/feature-status/feature-uids.ts b/packages/contentstack-utilities/src/feature-status/feature-uids.ts index d333ffb81f..fc55046e22 100644 --- a/packages/contentstack-utilities/src/feature-status/feature-uids.ts +++ b/packages/contentstack-utilities/src/feature-status/feature-uids.ts @@ -1,6 +1,6 @@ export const FEATURE = { ASSET_MANAGEMENT: 'amAssets', - // ASSET_SCANNING: 'asset_scanning', // uncomment when uid confirmed with platform team + ASSET_SCANNING: 'assetsScan', } as const; -export type FeatureUid = typeof FEATURE[keyof typeof FEATURE]; +export type FeatureUid = (typeof FEATURE)[keyof typeof FEATURE]; diff --git a/packages/contentstack/src/hooks/init/context-init.ts b/packages/contentstack/src/hooks/init/context-init.ts index 4d87969130..8b72ee68cb 100644 --- a/packages/contentstack/src/hooks/init/context-init.ts +++ b/packages/contentstack/src/hooks/init/context-init.ts @@ -9,5 +9,10 @@ export default function (opts): void { if (opts.id) { configHandler.set('currentCommandId', opts.id); } - this.config.context = new CsdxContext(opts, this.config); + const ctx = new CsdxContext(opts, this.config); + this.config.context = ctx; + // oclif v4 always recreates Config via Config.load(existingConfig), stripping custom + // properties like `context`. Storing it in options ensures it survives the spread: + // new Config({ ...opts.options, plugins }) in Config.load + (this.config.options as any).context = ctx; } diff --git a/packages/contentstack/src/hooks/prerun/plan-guard.ts b/packages/contentstack/src/hooks/prerun/plan-guard.ts index fd835d22b6..1ab99a10d8 100644 --- a/packages/contentstack/src/hooks/prerun/plan-guard.ts +++ b/packages/contentstack/src/hooks/prerun/plan-guard.ts @@ -1,12 +1,4 @@ -import { - configHandler, - assertFeatureEnabled, - FeatureCtx, - FeatureStatus, - log, - handleAndLogError, - cliux, -} from '@contentstack/cli-utilities'; +import { configHandler, isFeatureEnabled, FeatureCtx, FeatureStatus, log } from '@contentstack/cli-utilities'; function getArgvFlag(argv: string[], ...names: string[]): string | undefined { for (let i = 0; i < argv.length; i++) { @@ -23,30 +15,17 @@ function getArgvFlag(argv: string[], ...names: string[]): string | undefined { return undefined; } -export default async function (opts: { Command?: { id?: string }; argv?: string[]; config?: any }): Promise { +export default async function (opts: { Command?: { id?: string; planProtectedFeatures?: string[] }; argv?: string[]; config?: any }): Promise { const config = opts?.config ?? this.config; const commandId = opts?.Command?.id; const argv: string[] = opts?.argv ?? []; if (!commandId) return; - const planProtectedCommands: Record = - config?.context?.plugin?.config?.planProtectedCommands ?? {}; + const requiredFeatures: string[] = opts?.Command?.planProtectedFeatures ?? []; + if (requiredFeatures.length === 0) return; - const requiredFeatures: string[] = planProtectedCommands[commandId]; - if (!requiredFeatures || requiredFeatures.length === 0) return; - - // Resolve alias → management token - const aliasFromArgv = getArgvFlag(argv, '--alias', '-a'); - - // Clone uses different flag names — handle both alias forms - const cloneSourceAlias = - commandId === 'cm:stacks:clone' - ? getArgvFlag(argv, '--source-management-token-alias') ?? - getArgvFlag(argv, '--destination-management-token-alias') - : undefined; - - const alias = aliasFromArgv ?? cloneSourceAlias; + const alias = getArgvFlag(argv, '--alias', '-a'); let managementToken: string | undefined; let apiKeyFromAlias: string | undefined; if (alias) { @@ -69,10 +48,7 @@ export default async function (opts: { Command?: { id?: string }; argv?: string[ const apiKeyFromArgv = getArgvFlag(argv, '--stack-api-key', '-k'); const orgUid = configHandler.get('oauthOrgUid') as string | undefined; - const canCheckNow = - (!!managementToken && !!apiKeyFromAlias) || - isOAuth || - (isBasic && !!(apiKeyFromArgv || orgUid)); + const canCheckNow = (!!managementToken && !!apiKeyFromAlias) || isOAuth || (isBasic && !!(apiKeyFromArgv || orgUid)); if (!canCheckNow) { config.context.planCheckRequired = requiredFeatures; @@ -93,18 +69,22 @@ export default async function (opts: { Command?: { id?: string }; argv?: string[ }; const planStatus: Record = {}; + const failedFeatures: string[] = []; for (const featureUid of requiredFeatures) { try { - const result = await assertFeatureEnabled(featureUid, ctx); - planStatus[featureUid] = result; - log.debug(`[plan-guard] Feature "${featureUid}" is enabled.`, { module: 'plan-guard', commandId }); + planStatus[featureUid] = await isFeatureEnabled(featureUid, ctx); + log.debug(`[plan-guard] Feature "${featureUid}" status fetched.`, { module: 'plan-guard', commandId }); } catch (error) { - handleAndLogError(error, { module: 'plan-guard', commandId, featureUid }); - cliux.error((error as Error).message); - this.exit(1); - return; + log.warn(`[plan-guard] Could not fetch status for "${featureUid}": ${(error as Error).message}`, { + module: 'plan-guard', + commandId, + featureUid, + }); + failedFeatures.push(featureUid); } } - config.context.planStatus = planStatus; + if (failedFeatures.length > 0) { + config.context.planCheckRequired = failedFeatures; + } } diff --git a/packages/contentstack/tsconfig.json b/packages/contentstack/tsconfig.json index c12161b0f1..be6eadc8e2 100644 --- a/packages/contentstack/tsconfig.json +++ b/packages/contentstack/tsconfig.json @@ -8,7 +8,7 @@ "strict": false, "target": "es2017", "allowJs": true, - "sourceMap": false, + "sourceMap": true, "skipLibCheck": true, "esModuleInterop": true, "lib": [ From d833aa7876bc6160d2de0628011384099efcb319 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 20 Jul 2026 12:46:25 +0530 Subject: [PATCH 3/7] removed invalid urls set in region --- .talismanrc | 2 ++ .../src/commands/config/set/region.ts | 9 --------- .../src/interfaces/index.ts | 3 +-- .../src/utils/region-handler.ts | 19 +++---------------- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/.talismanrc b/.talismanrc index 169549a7cb..eae2f228bf 100644 --- a/.talismanrc +++ b/.talismanrc @@ -5,4 +5,6 @@ fileignoreconfig: checksum: 9360dfbce41aa9c8a62889f4821c37fc222b04b8b1c552d1dfbcd8f2854d49e0 - filename: packages/contentstack/src/hooks/prerun/plan-guard.ts checksum: 74eeb5c98e9ae48bf6f2918e75a7cfca23915396ffdd177b8716acba35c287da + - filename: packages/contentstack-config/src/utils/region-handler.ts + checksum: fe0713019d71cad642125de835a15a20784c961d6c83a9271f7287e73c1d4141 version: '1.0' diff --git a/packages/contentstack-config/src/commands/config/set/region.ts b/packages/contentstack-config/src/commands/config/set/region.ts index a0b8860813..0a640927f5 100644 --- a/packages/contentstack-config/src/commands/config/set/region.ts +++ b/packages/contentstack-config/src/commands/config/set/region.ts @@ -51,9 +51,6 @@ export default class RegionSetCommand extends BaseCommand Date: Thu, 23 Jul 2026 14:51:07 +0530 Subject: [PATCH 4/7] org plan check and endpoints auto update in cli config --- .talismanrc | 14 +- packages/contentstack-auth/README.md | 2 +- packages/contentstack-config/README.md | 5 +- .../src/commands/config/set/region.ts | 13 +- .../src/interfaces/index.ts | 2 +- .../src/utils/region-handler.ts | 54 +- .../test/unit/commands/region.test.ts | 81 +- packages/contentstack-utilities/package.json | 1 + .../src/feature-status/resolve-auth-host.ts | 26 +- .../src/feature-status/types.ts | 2 +- packages/contentstack-utilities/src/index.ts | 2 + .../src/region-endpoints.ts | 50 + .../src/region-refresh.ts | 23 + .../test/unit/region-endpoints.test.ts | 44 + .../test/unit/region-refresh.test.ts | 75 + .../test/unit/resolve-auth-host.test.ts | 46 + packages/contentstack/README.md | 1393 ++++++++++++++++- packages/contentstack/package.json | 4 +- .../src/hooks/init/region-refresh.ts | 13 + .../src/hooks/prerun/plan-guard.ts | 4 +- pnpm-lock.yaml | 46 +- 21 files changed, 1750 insertions(+), 150 deletions(-) create mode 100644 packages/contentstack-utilities/src/region-endpoints.ts create mode 100644 packages/contentstack-utilities/src/region-refresh.ts create mode 100644 packages/contentstack-utilities/test/unit/region-endpoints.test.ts create mode 100644 packages/contentstack-utilities/test/unit/region-refresh.test.ts create mode 100644 packages/contentstack-utilities/test/unit/resolve-auth-host.test.ts create mode 100644 packages/contentstack/src/hooks/init/region-refresh.ts diff --git a/.talismanrc b/.talismanrc index eae2f228bf..0830a02c31 100644 --- a/.talismanrc +++ b/.talismanrc @@ -6,5 +6,17 @@ fileignoreconfig: - filename: packages/contentstack/src/hooks/prerun/plan-guard.ts checksum: 74eeb5c98e9ae48bf6f2918e75a7cfca23915396ffdd177b8716acba35c287da - filename: packages/contentstack-config/src/utils/region-handler.ts - checksum: fe0713019d71cad642125de835a15a20784c961d6c83a9271f7287e73c1d4141 + checksum: cfa791c43bda0e297d655e159b8d0e8a34c698d6062a2e5e864c199d8f953406 + - filename: packages/contentstack-utilities/test/unit/region-endpoints.test.ts + checksum: fe37fa11a72d154e6bfeef38a18039c68cf3cc4446183cb012596ad8c7ea5083 + - filename: packages/contentstack-config/test/unit/commands/region.test.ts + checksum: 1ef835ff21d46107d42ea331d95bb346df49bae830658ed3b57146fca3858802 + - filename: packages/contentstack-utilities/src/region-endpoints.ts + checksum: a62589aacc21904ea8cf2af499a65cd7356dc76f0b2880e4caa42a25302ecc60 + - filename: packages/contentstack-utilities/test/unit/resolve-auth-host.test.ts + checksum: 19557d2905561d3dda94ebd68af342ecc646d2b5bbce3c1552bb9aea90f4bbbc + - filename: packages/contentstack-utilities/test/unit/region-refresh.test.ts + checksum: 1edac51f260df3ce28b538146e607d77742daed2364c25b4a922853b99f6d912 + - filename: packages/contentstack/README.md + checksum: f55c2719e1e10de809e89243809c11e2520d9f8a53429039435eb893c5fe03ec version: '1.0' diff --git a/packages/contentstack-auth/README.md b/packages/contentstack-auth/README.md index 035e761299..c2ee3ad478 100644 --- a/packages/contentstack-auth/README.md +++ b/packages/contentstack-auth/README.md @@ -18,7 +18,7 @@ $ npm install -g @contentstack/cli-auth $ csdx COMMAND running command... $ csdx (--version) -@contentstack/cli-auth/1.8.4 darwin-arm64 node-v22.21.1 +@contentstack/cli-auth/1.8.4 darwin-arm64 node-v22.13.1 $ csdx --help [COMMAND] USAGE $ csdx COMMAND diff --git a/packages/contentstack-config/README.md b/packages/contentstack-config/README.md index a6487d05e1..c6b0c438bd 100644 --- a/packages/contentstack-config/README.md +++ b/packages/contentstack-config/README.md @@ -18,7 +18,7 @@ $ npm install -g @contentstack/cli-config $ csdx COMMAND running command... $ csdx (--version) -@contentstack/cli-config/1.20.5 darwin-arm64 node-v22.21.1 +@contentstack/cli-config/1.20.5 darwin-arm64 node-v22.13.1 $ csdx --help [COMMAND] USAGE $ csdx COMMAND @@ -457,7 +457,7 @@ Set region for CLI ``` USAGE $ csdx config:set:region [REGION] [-d -m --ui-host -n ] [--developer-hub ] - [--personalize ] [--launch ] [--studio ] + [--personalize ] [--launch ] [--studio ] [--auth-api ] ARGUMENTS [REGION] Name for the region @@ -468,6 +468,7 @@ FLAGS -m, --cma= Custom host to set for content management API, , if this flag is added then cda, ui-host and name flags are required -n, --name= Name for the region, if this flag is added then cda, cma and ui-host flags are required + --auth-api= Custom host to set for Auth API --developer-hub= Custom host to set for Developer hub API --launch= Custom host to set for Launch API --personalize= Custom host to set for Personalize API diff --git a/packages/contentstack-config/src/commands/config/set/region.ts b/packages/contentstack-config/src/commands/config/set/region.ts index 0a640927f5..48db9b94fe 100644 --- a/packages/contentstack-config/src/commands/config/set/region.ts +++ b/packages/contentstack-config/src/commands/config/set/region.ts @@ -129,7 +129,16 @@ export default class RegionSetCommand extends BaseCommand; } export interface Limit { diff --git a/packages/contentstack-config/src/utils/region-handler.ts b/packages/contentstack-config/src/utils/region-handler.ts index a5474ae271..8f9e5671b0 100644 --- a/packages/contentstack-config/src/utils/region-handler.ts +++ b/packages/contentstack-config/src/utils/region-handler.ts @@ -1,5 +1,4 @@ -import { configHandler } from '@contentstack/cli-utilities'; -import { getContentstackEndpoint } from '@contentstack/utils'; +import { configHandler, resolveCanonicalEndpoints, buildRegionFromEndpoints } from '@contentstack/cli-utilities'; import { Region, RegionsMap } from '../interfaces'; function validURL(str) { @@ -23,28 +22,10 @@ function validURL(str) { * @returns {object} Region object with all necessary URLs */ function getRegionObject(regionKey: string): Region { - try { - // getContentstackEndpoint handles all aliases defined in regions.json - const endpoints = getContentstackEndpoint(regionKey) as any; - - if (typeof endpoints === 'string') { - throw new Error('Invalid endpoint response'); - } - - return { - name: regionKey, - cma: endpoints.contentManagement, - cda: endpoints.contentDelivery, - uiHost: endpoints.application, - developerHubUrl: endpoints.developerHub, - launchHubUrl: endpoints.launch, - personalizeUrl: endpoints.personalizeManagement, - composableStudioUrl: endpoints.composableStudio, - authUrl: endpoints.auth, - }; - } catch { - return null; - } + // resolveCanonicalEndpoints handles all aliases defined in regions.json + const endpoints = resolveCanonicalEndpoints(regionKey); + if (!endpoints) return null; + return buildRegionFromEndpoints(regionKey, endpoints) as Region; } /** @@ -147,19 +128,22 @@ class UserConfig { * @returns { object } JSON object with only valid keys for region */ sanitizeRegionObject(regionObject) { - const sanitizedRegion = { - cma: regionObject.cma, - cda: regionObject.cda, - uiHost: regionObject.uiHost, - name: regionObject.name, - developerHubUrl: regionObject['developerHubUrl'], - personalizeUrl: regionObject['personalizeUrl'], - launchHubUrl: regionObject['launchHubUrl'], - composableStudioUrl: regionObject['composableStudioUrl'], - authUrl: regionObject['authUrl'], + // endpoints is the single source of truth — every friendly field (cma, cda, ..., + // authUrl) is derived from it below via buildRegionFromEndpoints, same as named + // regions. Falls back to synthesizing endpoints from the individual raw fields + // when the caller didn't supply one directly. + const endpoints = regionObject['endpoints'] ?? { + contentManagement: regionObject.cma, + contentDelivery: regionObject.cda, + application: regionObject.uiHost, + developerHub: regionObject['developerHubUrl'], + launch: regionObject['launchHubUrl'], + personalizeManagement: regionObject['personalizeUrl'], + composableStudio: regionObject['composableStudioUrl'], + auth: regionObject['authUrl'], }; - return sanitizedRegion; + return buildRegionFromEndpoints(regionObject.name, endpoints); } } diff --git a/packages/contentstack-config/test/unit/commands/region.test.ts b/packages/contentstack-config/test/unit/commands/region.test.ts index 82d812647f..f3f85b4355 100644 --- a/packages/contentstack-config/test/unit/commands/region.test.ts +++ b/packages/contentstack-config/test/unit/commands/region.test.ts @@ -299,7 +299,86 @@ describe('Region command', function () { composableStudioUrl: 'https://custom-composable-studio.com', }; const result = UserConfig.setCustomRegion(customRegion); - expect(result).to.deep.equal(customRegion); + expect(result.cma).to.equal(customRegion.cma); + expect(result.cda).to.equal(customRegion.cda); + expect(result.uiHost).to.equal(customRegion.uiHost); + expect(result.name).to.equal(customRegion.name); + expect(result.developerHubUrl).to.equal(customRegion.developerHubUrl); + expect(result.personalizeUrl).to.equal(customRegion.personalizeUrl); + expect(result.launchHubUrl).to.equal(customRegion.launchHubUrl); + expect(result.composableStudioUrl).to.equal(customRegion.composableStudioUrl); + }); + + it('should include a matching endpoints object for a custom region without an auth host', function () { + const customRegion = { + cma: 'https://custom-cma.com', + cda: 'https://custom-cda.com', + uiHost: 'https://custom-ui.com', + name: 'Custom Region', + developerHubUrl: 'https://custom-developer-hub.com', + personalizeUrl: 'https://custom-personalize.com', + launchHubUrl: 'https://custom-launch.com', + composableStudioUrl: 'https://custom-composable-studio.com', + }; + const result = UserConfig.setCustomRegion(customRegion); + expect(result.endpoints).to.deep.equal({ + contentManagement: customRegion.cma, + contentDelivery: customRegion.cda, + application: customRegion.uiHost, + developerHub: customRegion.developerHubUrl, + launch: customRegion.launchHubUrl, + personalizeManagement: customRegion.personalizeUrl, + composableStudio: customRegion.composableStudioUrl, + auth: undefined, + }); + }); + + it('should preserve an explicitly supplied endpoints object for a custom region', function () { + const endpoints = { + contentManagement: 'https://custom-cma.com', + contentDelivery: 'https://custom-cda.com', + application: 'https://custom-ui.com', + auth: 'https://custom-auth.com', + }; + const customRegion = { + cma: 'https://custom-cma.com', + cda: 'https://custom-cda.com', + uiHost: 'https://custom-ui.com', + name: 'Custom Region', + endpoints, + }; + const result = UserConfig.setCustomRegion(customRegion); + expect(result.cma).to.equal(endpoints.contentManagement); + expect(result.cda).to.equal(endpoints.contentDelivery); + expect(result.uiHost).to.equal(endpoints.application); + expect(result.endpoints).to.deep.equal(endpoints); + }); + + it('should not fall back to a stale legacy authUrl input once endpoints.auth is supplied', function () { + // endpoints is the single source of truth: a stale/mismatched top-level + // authUrl on the raw input is never used — only endpoints.auth matters. + const customRegion = { + cma: 'https://custom-cma.com', + cda: 'https://custom-cda.com', + uiHost: 'https://custom-ui.com', + name: 'Custom Region', + authUrl: 'https://stale-auth.com', + endpoints: { + contentManagement: 'https://custom-cma.com', + contentDelivery: 'https://custom-cda.com', + application: 'https://custom-ui.com', + auth: 'https://correct-auth.com', + }, + }; + const result = UserConfig.setCustomRegion(customRegion); + expect(result.endpoints.auth).to.equal('https://correct-auth.com'); + }); + + it('should include the full endpoints passthrough (incl. auth) for named regions', function () { + const result = UserConfig.setRegion('NA'); + expect(result.endpoints).to.be.an('object'); + expect(result.endpoints.auth).to.equal('https://auth-api.contentstack.com'); + expect(result.endpoints.contentManagement).to.equal(result.cma); }); it('should sanitize region object to only include valid properties', function () { diff --git a/packages/contentstack-utilities/package.json b/packages/contentstack-utilities/package.json index f3b19cd8cf..0160f448b9 100644 --- a/packages/contentstack-utilities/package.json +++ b/packages/contentstack-utilities/package.json @@ -29,6 +29,7 @@ "dependencies": { "@contentstack/management": "~1.30.1", "@contentstack/marketplace-sdk": "^1.5.1", + "@contentstack/utils": "~1.9.1", "@oclif/core": "^4.11.4", "axios": "^1.16.1", "chalk": "^4.1.2", diff --git a/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts b/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts index a86e4786ee..04e2d29ab4 100644 --- a/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts +++ b/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts @@ -1,8 +1,28 @@ import configHandler from '../config-handler'; -export function resolveAuthHost(ctx?: { region?: { authUrl?: string } }): string { - const region = (ctx?.region ?? configHandler.get('region')) as { authUrl?: string } | undefined; - const authUrl = region?.authUrl; +// Mirrors config:set:region's own custom-region fallback (region.ts's transformUrl) — +// same heuristic, applied retroactively for a custom region that predates auth-api. +// Computed fresh on each call, never persisted back to config. +function deriveAuthFromCma(cma: string): string { + let transformed = cma.replace('api', 'auth-api'); + if (transformed.startsWith('http')) { + transformed = transformed.split('//')[1]; + } + transformed = transformed.replace(/^dev\d+/, 'dev'); + transformed = transformed.endsWith('io') ? transformed.replace('io', 'com') : transformed; + return `https://${transformed}`; +} + +export function resolveAuthHost(ctx?: { region?: { endpoints?: { auth?: string }; cma?: string } }): string { + const region = (ctx?.region ?? configHandler.get('region')) as + | { endpoints?: { auth?: string }; cma?: string } + | undefined; + + let authUrl = region?.endpoints?.auth; + if (!authUrl && region?.cma) { + authUrl = deriveAuthFromCma(region.cma); + } + if (!authUrl) { throw new Error( 'PLAN_CHECK: Auth host is not configured for the current region. ' + diff --git a/packages/contentstack-utilities/src/feature-status/types.ts b/packages/contentstack-utilities/src/feature-status/types.ts index 136c3fee66..1ef24c0f39 100644 --- a/packages/contentstack-utilities/src/feature-status/types.ts +++ b/packages/contentstack-utilities/src/feature-status/types.ts @@ -12,5 +12,5 @@ export interface FeatureCtx { orgUid?: string; managementToken?: string; authToken?: string; - region?: { authUrl?: string; name?: string; cma?: string }; + region?: { name?: string; cma?: string; endpoints?: { auth?: string } }; } diff --git a/packages/contentstack-utilities/src/index.ts b/packages/contentstack-utilities/src/index.ts index 66ed0e16a3..23f68491f3 100644 --- a/packages/contentstack-utilities/src/index.ts +++ b/packages/contentstack-utilities/src/index.ts @@ -82,3 +82,5 @@ export { default as authenticationHandler } from './authentication-handler'; export {v2Logger as log, cliErrorHandler, handleAndLogError, getLogPath} from './logger/log' export * from './feature-status'; +export * from './region-endpoints'; +export { refreshRegionEndpoints } from './region-refresh'; diff --git a/packages/contentstack-utilities/src/region-endpoints.ts b/packages/contentstack-utilities/src/region-endpoints.ts new file mode 100644 index 0000000000..39118c42bc --- /dev/null +++ b/packages/contentstack-utilities/src/region-endpoints.ts @@ -0,0 +1,50 @@ +import { getContentstackEndpoint } from '@contentstack/utils'; + +export interface RegionEndpoints { + [key: string]: string; +} + +export interface CanonicalRegion { + name: string; + cma: string; + cda: string; + uiHost: string; + developerHubUrl: string; + launchHubUrl: string; + personalizeUrl: string; + composableStudioUrl: string; + endpoints: RegionEndpoints; +} + +/** + * Resolve the canonical endpoint set for a region name/alias via @contentstack/utils. + * Returns null when the name isn't a recognized region (e.g. a custom region name). + */ +export function resolveCanonicalEndpoints(name: string): RegionEndpoints | null { + try { + const endpoints = getContentstackEndpoint(name) as unknown; + if (!endpoints || typeof endpoints === 'string') return null; + return endpoints as RegionEndpoints; + } catch { + return null; + } +} + +/** + * Build a region object from a raw endpoints map, keeping the existing named + * fields (cma/cda/uiHost/...) plus a full raw `endpoints` passthrough so any + * future endpoint Contentstack adds is available without further code changes. + */ +export function buildRegionFromEndpoints(name: string, endpoints: RegionEndpoints): CanonicalRegion { + return { + name, + cma: endpoints.contentManagement, + cda: endpoints.contentDelivery, + uiHost: endpoints.application, + developerHubUrl: endpoints.developerHub, + launchHubUrl: endpoints.launch, + personalizeUrl: endpoints.personalizeManagement, + composableStudioUrl: endpoints.composableStudio, + endpoints, + }; +} diff --git a/packages/contentstack-utilities/src/region-refresh.ts b/packages/contentstack-utilities/src/region-refresh.ts new file mode 100644 index 0000000000..ef84165651 --- /dev/null +++ b/packages/contentstack-utilities/src/region-refresh.ts @@ -0,0 +1,23 @@ +import configHandler from './config-handler'; +import { resolveCanonicalEndpoints, buildRegionFromEndpoints } from './region-endpoints'; + +/** + * Self-heals the persisted region config on every CLI invocation so that + * customers who set their region on an older CLI version (before a field + * like authUrl existed) get it backfilled without re-running + * `csdx config:set:region`. Only named/built-in regions are touched — a + * custom region's name won't resolve via resolveCanonicalEndpoints, so it's + * left untouched. Never throws. + */ +export function refreshRegionEndpoints(): void { + const stored = configHandler.get('region') as ({ name?: string } & Record) | undefined; + if (!stored?.name) return; + + const endpoints = resolveCanonicalEndpoints(stored.name); + if (!endpoints) return; + + const merged = { ...stored, ...buildRegionFromEndpoints(stored.name, endpoints) }; + if (JSON.stringify(merged) !== JSON.stringify(stored)) { + configHandler.set('region', merged); + } +} diff --git a/packages/contentstack-utilities/test/unit/region-endpoints.test.ts b/packages/contentstack-utilities/test/unit/region-endpoints.test.ts new file mode 100644 index 0000000000..d88a00ce9d --- /dev/null +++ b/packages/contentstack-utilities/test/unit/region-endpoints.test.ts @@ -0,0 +1,44 @@ +import { expect } from 'chai'; +import { resolveCanonicalEndpoints, buildRegionFromEndpoints } from '../../src/region-endpoints'; + +describe('region-endpoints', () => { + describe('resolveCanonicalEndpoints', () => { + it('should resolve endpoints for a known region name', () => { + const endpoints = resolveCanonicalEndpoints('NA'); + expect(endpoints).to.not.be.null; + expect(endpoints.contentManagement).to.equal('https://api.contentstack.io'); + expect(endpoints.auth).to.equal('https://auth-api.contentstack.com'); + }); + + it('should resolve endpoints via a known alias', () => { + const endpoints = resolveCanonicalEndpoints('us'); + expect(endpoints).to.not.be.null; + expect(endpoints.contentManagement).to.equal('https://api.contentstack.io'); + }); + + it('should return null for an unrecognized/custom region name', () => { + const endpoints = resolveCanonicalEndpoints('My Totally Custom Region'); + expect(endpoints).to.be.null; + }); + }); + + describe('buildRegionFromEndpoints', () => { + it('should map named fields and include the full raw endpoints passthrough', () => { + const endpoints = resolveCanonicalEndpoints('EU'); + const region = buildRegionFromEndpoints('EU', endpoints); + + expect(region.name).to.equal('EU'); + expect(region.cma).to.equal(endpoints.contentManagement); + expect(region.cda).to.equal(endpoints.contentDelivery); + expect(region.uiHost).to.equal(endpoints.application); + expect(region.developerHubUrl).to.equal(endpoints.developerHub); + expect(region.launchHubUrl).to.equal(endpoints.launch); + expect(region.personalizeUrl).to.equal(endpoints.personalizeManagement); + expect(region.composableStudioUrl).to.equal(endpoints.composableStudio); + expect(region.endpoints).to.equal(endpoints); + expect(region.endpoints.auth).to.equal(endpoints.auth); + // future fields not yet named on the interface remain reachable via `.endpoints` + expect(region.endpoints.assets).to.be.a('string'); + }); + }); +}); diff --git a/packages/contentstack-utilities/test/unit/region-refresh.test.ts b/packages/contentstack-utilities/test/unit/region-refresh.test.ts new file mode 100644 index 0000000000..bffec8b7df --- /dev/null +++ b/packages/contentstack-utilities/test/unit/region-refresh.test.ts @@ -0,0 +1,75 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import configHandler from '../../src/config-handler'; +import { refreshRegionEndpoints } from '../../src/region-refresh'; + +describe('refreshRegionEndpoints', () => { + let getStub: sinon.SinonStub; + let setStub: sinon.SinonStub; + + afterEach(() => { + getStub?.restore(); + setStub?.restore(); + }); + + it('should do nothing when no region is stored', () => { + getStub = sinon.stub(configHandler, 'get').returns(undefined); + setStub = sinon.stub(configHandler, 'set'); + + refreshRegionEndpoints(); + + expect(setStub.called).to.be.false; + }); + + it('should backfill and persist a named region missing endpoints', () => { + const stale = { + name: 'NA', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com', + }; + getStub = sinon.stub(configHandler, 'get').callsFake((key) => (key === 'region' ? stale : undefined)); + setStub = sinon.stub(configHandler, 'set'); + + refreshRegionEndpoints(); + + expect(setStub.calledOnce).to.be.true; + const [key, merged] = setStub.firstCall.args; + expect(key).to.equal('region'); + expect(merged.endpoints).to.be.an('object'); + expect(merged.endpoints.auth).to.equal('https://auth-api.contentstack.com'); + }); + + it('should not write when the named region is already fully healed', () => { + // First refresh to compute the canonical/healed shape, without touching config. + const stale = { name: 'AWS-NA' }; + getStub = sinon.stub(configHandler, 'get').callsFake((key) => (key === 'region' ? stale : undefined)); + setStub = sinon.stub(configHandler, 'set'); + refreshRegionEndpoints(); + const healed = setStub.firstCall.args[1]; + + getStub.restore(); + setStub.restore(); + + getStub = sinon.stub(configHandler, 'get').callsFake((key) => (key === 'region' ? healed : undefined)); + setStub = sinon.stub(configHandler, 'set'); + + refreshRegionEndpoints(); + + expect(setStub.called).to.be.false; + }); + + it('should leave a custom/unrecognized region untouched', () => { + const custom = { + name: 'My Totally Custom Region', + cma: 'https://custom-cma.com', + cda: 'https://custom-cda.com', + uiHost: 'https://custom-ui.com', + }; + getStub = sinon.stub(configHandler, 'get').callsFake((key) => (key === 'region' ? custom : undefined)); + setStub = sinon.stub(configHandler, 'set'); + + expect(() => refreshRegionEndpoints()).to.not.throw(); + expect(setStub.called).to.be.false; + }); +}); diff --git a/packages/contentstack-utilities/test/unit/resolve-auth-host.test.ts b/packages/contentstack-utilities/test/unit/resolve-auth-host.test.ts new file mode 100644 index 0000000000..0aaffc8d90 --- /dev/null +++ b/packages/contentstack-utilities/test/unit/resolve-auth-host.test.ts @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import configHandler from '../../src/config-handler'; +import { resolveAuthHost } from '../../src/feature-status/resolve-auth-host'; + +describe('resolveAuthHost', () => { + let getStub: sinon.SinonStub; + + afterEach(() => { + getStub?.restore(); + }); + + it('should use endpoints.auth when present on the passed context', () => { + const host = resolveAuthHost({ region: { endpoints: { auth: 'https://auth-api.contentstack.com/' } } }); + expect(host).to.equal('https://auth-api.contentstack.com'); + }); + + it('should fall back to deriving from cma when endpoints.auth is missing', () => { + const host = resolveAuthHost({ region: { cma: 'https://api.contentstack.io' } }); + expect(host).to.equal('https://auth-api.contentstack.com'); + }); + + it('should not persist the derived fallback', () => { + const setStub = sinon.stub(configHandler, 'set'); + resolveAuthHost({ region: { cma: 'https://api.contentstack.io' } }); + expect(setStub.called).to.be.false; + setStub.restore(); + }); + + it('should read region from configHandler when no ctx.region is given', () => { + getStub = sinon + .stub(configHandler, 'get') + .callsFake((key) => (key === 'region' ? { endpoints: { auth: 'https://auth-api.contentstack.com' } } : undefined)); + const host = resolveAuthHost(); + expect(host).to.equal('https://auth-api.contentstack.com'); + }); + + it('should throw when both endpoints.auth and cma are missing', () => { + expect(() => resolveAuthHost({ region: {} })).to.throw(/PLAN_CHECK: Auth host is not configured/); + }); + + it('should throw when region is entirely absent', () => { + getStub = sinon.stub(configHandler, 'get').returns(undefined); + expect(() => resolveAuthHost()).to.throw(/PLAN_CHECK: Auth host is not configured/); + }); +}); diff --git a/packages/contentstack/README.md b/packages/contentstack/README.md index 2a6de2f304..761ff3663b 100644 --- a/packages/contentstack/README.md +++ b/packages/contentstack/README.md @@ -25,7 +25,7 @@ $ npm install -g @contentstack/cli $ csdx COMMAND running command... $ csdx (--version|-v) -@contentstack/cli/1.63.1 darwin-arm64 node-v22.21.1 +@contentstack/cli/1.65.0 darwin-arm64 node-v22.13.1 $ csdx --help [COMMAND] USAGE $ csdx COMMAND @@ -36,17 +36,26 @@ USAGE # Commands +* [`csdx audit`](#csdx-audit) +* [`csdx audit:fix`](#csdx-auditfix) * [`csdx auth:login`](#csdx-authlogin) * [`csdx auth:logout`](#csdx-authlogout) * [`csdx auth:tokens`](#csdx-authtokens) * [`csdx auth:tokens:add [-a ] [--delivery] [--management] [-e ] [-k ] [-y] [--token ]`](#csdx-authtokensadd--a-value---delivery---management--e-value--k-value--y---token-value) * [`csdx auth:tokens:remove`](#csdx-authtokensremove) * [`csdx auth:whoami`](#csdx-authwhoami) -* [`csdx cm:assets:publish [-a ] [--retry-failed ] [-e ] [--folder-uid ] [--bulk-publish ] [-c ] [-y] [--locales ] [--branch ] [--delivery-token ] [--source-env ]`](#csdx-cmassetspublish--a-value---retry-failed-value--e-value---folder-uid-value---bulk-publish-value--c-value--y---locales-value---branch-value---delivery-token-value---source-env-value) +* [`csdx cm:assets:publish [-a ] [--retry-failed ] [-e ] [--folder-uid ] [--backup-dir ] [--bulk-publish ] [-c ] [-y] [--locales ] [--branch ] [--delivery-token ] [--source-env ]`](#csdx-cmassetspublish--a-value---retry-failed-value--e-value---folder-uid-value---backup-dir-value---bulk-publish-value--c-value--y---locales-value---branch-value---delivery-token-value---source-env-value) * [`csdx cm:assets:unpublish`](#csdx-cmassetsunpublish) +* [`csdx cm:bootstrap`](#csdx-cmbootstrap) +* [`csdx cm:branches`](#csdx-cmbranches) +* [`csdx cm:branches:create`](#csdx-cmbranchescreate) +* [`csdx cm:branches:delete [-uid ] [-k ]`](#csdx-cmbranchesdelete--uid-value--k-value) +* [`csdx cm:branches:diff [--base-branch ] [--compare-branch ] [-k ][--module ] [--format ] [--csv-path ]`](#csdx-cmbranchesdiff---base-branch-value---compare-branch-value--k-value--module-value---format-value---csv-path-value) +* [`csdx cm:branches:merge [-k ][--compare-branch ] [--no-revert] [--export-summary-path ] [--use-merge-summary ] [--comment ] [--base-branch ]`](#csdx-cmbranchesmerge--k-value--compare-branch-value---no-revert---export-summary-path-value---use-merge-summary-value---comment-value---base-branch-value) +* [`csdx cm:branches:merge-status -k --merge-uid `](#csdx-cmbranchesmerge-status--k-value---merge-uid-value) * [`csdx cm:bulk-publish`](#csdx-cmbulk-publish) * [`csdx cm:entries:update-and-publish [-a ] [--retry-failed ] [--bulk-publish ] [--content-types ] [-t ] [-e ] [-c ] [-y] [--locales ] [--branch ]`](#csdx-cmentriesupdate-and-publish--a-value---retry-failed-value---bulk-publish-value---content-types-value--t-value--e-value--c-value--y---locales-value---branch-value) -* [`csdx cm:assets:publish [-a ] [--retry-failed ] [-e ] [--folder-uid ] [--bulk-publish ] [-c ] [-y] [--locales ] [--branch ] [--delivery-token ] [--source-env ]`](#csdx-cmassetspublish--a-value---retry-failed-value--e-value---folder-uid-value---bulk-publish-value--c-value--y---locales-value---branch-value---delivery-token-value---source-env-value) +* [`csdx cm:assets:publish [-a ] [--retry-failed ] [-e ] [--folder-uid ] [--backup-dir ] [--bulk-publish ] [-c ] [-y] [--locales ] [--branch ] [--delivery-token ] [--source-env ]`](#csdx-cmassetspublish--a-value---retry-failed-value--e-value---folder-uid-value---backup-dir-value---bulk-publish-value--c-value--y---locales-value---branch-value---delivery-token-value---source-env-value) * [`csdx cm:bulk-publish:clear`](#csdx-cmbulk-publishclear) * [`csdx cm:bulk-publish:configure`](#csdx-cmbulk-publishconfigure) * [`csdx cm:bulk-publish:cross-publish [-a ] [--retry-failed ] [--bulk-publish ] [--content-type ] [--locales ] [--source-env ] [--environments ] [--delivery-token ] [-c ] [-y] [--branch ] [--onlyAssets] [--onlyEntries] [--include-variants]`](#csdx-cmbulk-publishcross-publish--a-value---retry-failed-value---bulk-publish-value---content-type-value---locales-value---source-env-value---environments-value---delivery-token-value--c-value--y---branch-value---onlyassets---onlyentries---include-variants) @@ -63,10 +72,25 @@ USAGE * [`csdx cm:entries:publish-only-unpublished [-a ] [--retry-failed ] [--bulk-publish ] [--source-env ] [--content-types ] [--locales ] [-e ] [-c ] [-y] [--branch ]`](#csdx-cmentriespublish-only-unpublished--a-value---retry-failed-value---bulk-publish-value---source-env-value---content-types-value---locales-value--e-value--c-value--y---branch-value) * [`csdx cm:entries:unpublish`](#csdx-cmentriesunpublish) * [`csdx cm:entries:update-and-publish [-a ] [--retry-failed ] [--bulk-publish ] [--content-types ] [-t ] [-e ] [-c ] [-y] [--locales ] [--branch ]`](#csdx-cmentriesupdate-and-publish--a-value---retry-failed-value---bulk-publish-value---content-types-value--t-value--e-value--c-value--y---locales-value---branch-value) +* [`csdx cm:stacks:export [-c ] [-k ] [-d ] [-a ] [--module ] [--content-types ] [--branch ] [--secured-assets]`](#csdx-cmstacksexport--c-value--k-value--d-value--a-value---module-value---content-types-value---branch-value---secured-assets) +* [`csdx cm:export-to-csv`](#csdx-cmexport-to-csv) +* [`csdx cm:stacks:import [-c ] [-k ] [-d ] [-a ] [--module ] [--backup-dir ] [--branch ] [--import-webhook-status disable|current]`](#csdx-cmstacksimport--c-value--k-value--d-value--a-value---module-value---backup-dir-value---branch-value---import-webhook-status-disablecurrent) +* [`csdx cm:stacks:import-setup [-k ] [-d ] [-a ] [--modules ]`](#csdx-cmstacksimport-setup--k-value--d-value--a-value---modules-valuevalue) +* [`csdx cm:stacks:migration [-k ] [-a ] [--file-path ] [--branch ] [--config-file ] [--config ] [--multiple]`](#csdx-cmstacksmigration--k-value--a-value---file-path-value---branch-value---config-file-value---config-value---multiple) +* [`csdx cm:stacks:seed [--repo ] [--org ] [-k ] [-n ] [-y] [-s ] [--locale ]`](#csdx-cmstacksseed---repo-value---org-value--k-value--n-value--y--s-value---locale-value) +* [`csdx cm:stacks:clone [--source-branch ] [--target-branch ] [--source-management-token-alias ] [--destination-management-token-alias ] [-n ] [--type a|b] [--source-stack-api-key ] [--destination-stack-api-key ] [--import-webhook-status disable|current]`](#csdx-cmstacksclone---source-branch-value---target-branch-value---source-management-token-alias-value---destination-management-token-alias-value--n-value---type-ab---source-stack-api-key-value---destination-stack-api-key-value---import-webhook-status-disablecurrent) +* [`csdx cm:stacks:audit`](#csdx-cmstacksaudit) +* [`csdx cm:stacks:audit:fix`](#csdx-cmstacksauditfix) +* [`csdx cm:stacks:clone [--source-branch ] [--target-branch ] [--source-management-token-alias ] [--destination-management-token-alias ] [-n ] [--type a|b] [--source-stack-api-key ] [--destination-stack-api-key ] [--import-webhook-status disable|current]`](#csdx-cmstacksclone---source-branch-value---target-branch-value---source-management-token-alias-value---destination-management-token-alias-value--n-value---type-ab---source-stack-api-key-value---destination-stack-api-key-value---import-webhook-status-disablecurrent) +* [`csdx cm:stacks:export [-c ] [-k ] [-d ] [-a ] [--module ] [--content-types ] [--branch ] [--secured-assets]`](#csdx-cmstacksexport--c-value--k-value--d-value--a-value---module-value---content-types-value---branch-value---secured-assets) +* [`csdx cm:stacks:import [-c ] [-k ] [-d ] [-a ] [--module ] [--backup-dir ] [--branch ] [--import-webhook-status disable|current]`](#csdx-cmstacksimport--c-value--k-value--d-value--a-value---module-value---backup-dir-value---branch-value---import-webhook-status-disablecurrent) +* [`csdx cm:stacks:import-setup [-k ] [-d ] [-a ] [--modules ]`](#csdx-cmstacksimport-setup--k-value--d-value--a-value---modules-valuevalue) +* [`csdx cm:stacks:migration [-k ] [-a ] [--file-path ] [--branch ] [--config-file ] [--config ] [--multiple]`](#csdx-cmstacksmigration--k-value--a-value---file-path-value---branch-value---config-file-value---config-value---multiple) * [`csdx cm:stacks:publish`](#csdx-cmstackspublish) * [`csdx cm:stacks:publish-clear-logs`](#csdx-cmstackspublish-clear-logs) * [`csdx cm:stacks:publish-configure`](#csdx-cmstackspublish-configure) * [`csdx cm:stacks:publish-revert`](#csdx-cmstackspublish-revert) +* [`csdx cm:stacks:seed [--repo ] [--org ] [-k ] [-n ] [-y] [-s ] [--locale ]`](#csdx-cmstacksseed---repo-value---org-value--k-value--n-value--y--s-value---locale-value) * [`csdx csdx cm:stacks:unpublish [-a ] [-e ] [-c ] [-y] [--locale ] [--branch ] [--retry-failed ] [--bulk-unpublish ] [--content-type ] [--delivery-token ] [--only-assets] [--only-entries]`](#csdx-csdx-cmstacksunpublish--a-value--e-value--c-value--y---locale-value---branch-value---retry-failed-value---bulk-unpublish-value---content-type-value---delivery-token-value---only-assets---only-entries) * [`csdx config:get:base-branch`](#csdx-configgetbase-branch) * [`csdx config:get:ea-header`](#csdx-configgetea-header) @@ -110,6 +134,115 @@ USAGE * [`csdx tokens`](#csdx-tokens) * [`csdx whoami`](#csdx-whoami) +## `csdx audit` + +Perform audits and find possible errors in the exported Contentstack data + +``` +USAGE + $ csdx audit [-c ] [-d ] [--show-console-output] [--report-path ] [--modules + content-types|global-fields|entries|extensions|workflows|custom-roles|assets|field-rules|composable-studio...] + [--columns ] [--sort ] [--filter ] [--csv] [--no-truncate] [--no-header] [--output + csv|json|yaml] + +FLAGS + --modules=