diff --git a/packages/contentstack-bootstrap/src/bootstrap/index.ts b/packages/contentstack-bootstrap/src/bootstrap/index.ts index dca6a7220..c68bcdcaa 100644 --- a/packages/contentstack-bootstrap/src/bootstrap/index.ts +++ b/packages/contentstack-bootstrap/src/bootstrap/index.ts @@ -28,7 +28,7 @@ export interface SeedParams { stackAPIKey?: string; org?: string; stackName?: string; - yes?: string; + yes?: boolean; managementTokenAlias?: string | undefined; managementToken?: string | undefined; } @@ -95,7 +95,7 @@ export default class Bootstrap { cmd.push('-n', this.options.seedParams.stackName); } if (this.options.seedParams.yes) { - cmd.push('-y', this.options.seedParams.yes); + cmd.push('-y'); } if (this.options.seedParams.managementTokenAlias) { cmd.push('--alias', this.options.seedParams.managementTokenAlias); diff --git a/packages/contentstack-bootstrap/src/bootstrap/utils.ts b/packages/contentstack-bootstrap/src/bootstrap/utils.ts index 164f9a1ad..cd1da33e4 100644 --- a/packages/contentstack-bootstrap/src/bootstrap/utils.ts +++ b/packages/contentstack-bootstrap/src/bootstrap/utils.ts @@ -40,11 +40,11 @@ export const setupEnvironments = async ( if (!managementToken) { const managementBody = { token: { - name: 'sample app', - description: 'This is a sample management token.', + name: 'Compass Starter App', + description: 'This is a compass starter app management token.', scope: [ { - module: 'content_type', + module: '$all', acl: { read: true, write: true, @@ -58,8 +58,9 @@ export const setupEnvironments = async ( }, }, ], - expires_on: '3000-01-01', + expires_on: null, is_email_notification_enabled: false, + rate_limit_enabled: false, }, }; managementTokenResult = await managementAPIClient @@ -313,7 +314,7 @@ const envFileHandler = async ( }CONTENTSTACK_ENVIRONMENT=${environmentVariables.environment}${!isUSRegion && !customHost ? '\nCONTENTSTACK_REGION=' + region.name : '' }\nCONTENTSTACK_LIVE_PREVIEW=${livePreviewEnabled}\nCONTENTSTACK_LIVE_EDIT_TAGS=false\nCONTENTSTACK_API_HOST=${customHost ? customHost : managementAPIHost }${!isUSRegion && !customHost ? '\nCONTENTSTACK_REGION=' + region.name : '' - }\nCONTENTSTACK_APP_HOST=${appHost}\nCONTENTSTACK_MANAGEMENT_TOKEN=${managementTokenResult.uid + }\nCONTENTSTACK_APP_HOST=${appHost}\nCONTENTSTACK_MANAGEMENT_TOKEN=${managementTokenResult.token }\nCONTENTSTACK_HOST=${cdnHost}`; result = await writeEnvFile(content, filePath); break; diff --git a/packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts b/packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts index 1898338f4..39fd9994f 100644 --- a/packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts +++ b/packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts @@ -66,7 +66,7 @@ export default class BootstrapCommand extends Command { required: false, exclusive: ['stack-api-key'], }), - yes: flags.string({ + yes: flags.boolean({ description: '[Optional] Skip stack confirmation', char: 'y', required: false, @@ -123,7 +123,7 @@ export default class BootstrapCommand extends Command { }); } - const yes = bootstrapCommandFlags.yes as string; + const yes = bootstrapCommandFlags.yes as boolean; const appConfig: AppConfig = getAppLevelConfigByName(selectedAppName || selectedApp.configKey); const master_locale = appConfig.master_locale || DEFAULT_MASTER_LOCALE; @@ -147,7 +147,7 @@ export default class BootstrapCommand extends Command { if (stackAPIKey) seedParams.stackAPIKey = stackAPIKey; if (org) seedParams.org = org; if (stackName) seedParams.stackName = stackName; - if (yes) seedParams.yes = yes; + if (yes) seedParams.yes = true; if (managementTokenAlias) { seedParams.managementTokenAlias = managementTokenAlias; const listOfTokens = configHandler.get('tokens'); diff --git a/packages/contentstack-seed/README.md b/packages/contentstack-seed/README.md index 54dbca938..45c0fd2fb 100644 --- a/packages/contentstack-seed/README.md +++ b/packages/contentstack-seed/README.md @@ -10,22 +10,22 @@ To import content to your stack, you can choose from the following two sources: ## Commands -* [`csdx cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [--yes ] [--alias ] [--locale ]`](#csdx-cmstacksseed---repo-value---org-value---stack-api-key-value---stack-name-value---yes-value---alias-value---locale-value) +* [`csdx cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [-y] [--alias ] [--locale ]`](#csdx-cmstacksseed---repo-value---org-value---stack-api-key-value---stack-name-value---y---alias-value---locale-value) -## `csdx cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [--yes ] [--alias ] [--locale ]` +## `csdx cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [-y] [--alias ] [--locale ]` Create a stack from existing content types, entries, assets, etc ``` USAGE - $ csdx cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [--yes - ] [--alias ] [--locale ] + $ csdx cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [-y] + [--alias ] [--locale ] FLAGS -a, --alias= Alias of the management token -k, --stack-api-key= Provide stack API key to seed content to -n, --stack-name= Name of a new stack that needs to be created. - -y, --yes= [Optional] Skip the stack confirmation. + -y, --yes [Optional] Skip the stack confirmation. --org= Provide Organization UID to create a new stack --repo= GitHub organization name or GitHub user name/repository name. diff --git a/packages/contentstack-seed/src/commands/cm/stacks/seed.ts b/packages/contentstack-seed/src/commands/cm/stacks/seed.ts index c3beca01e..ddbfe55ae 100644 --- a/packages/contentstack-seed/src/commands/cm/stacks/seed.ts +++ b/packages/contentstack-seed/src/commands/cm/stacks/seed.ts @@ -14,7 +14,7 @@ export default class SeedCommand extends Command { ]; static usage = - 'cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [--yes ] [--alias ] [--locale ]'; + 'cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [-y] [--alias ] [--locale ]'; static flags: FlagInput = { repo: flags.string({ @@ -49,7 +49,7 @@ export default class SeedCommand extends Command { required: false, hidden: true, }), - yes: flags.string({ + yes: flags.boolean({ char: 'y', required: false, description: '[Optional] Skip the stack confirmation.', @@ -60,6 +60,7 @@ export default class SeedCommand extends Command { }), locale: flags.string({ description: 'Master Locale of the stack', + default: 'en-us', hidden: true, }), }; @@ -84,7 +85,7 @@ export default class SeedCommand extends Command { stackUid: seedFlags['stack-api-key'], stackName: seedFlags['stack-name'], fetchLimit: seedFlags['fetch-limit'], - skipStackConfirmation: seedFlags['yes'], + skipStackConfirmation: seedFlags.yes, isAuthenticated: isAuthenticated(), alias: managementTokenAlias, master_locale: seedFlags['locale'], diff --git a/packages/contentstack-seed/src/seed/github/client.ts b/packages/contentstack-seed/src/seed/github/client.ts index 2bd9a7013..066913fb7 100644 --- a/packages/contentstack-seed/src/seed/github/client.ts +++ b/packages/contentstack-seed/src/seed/github/client.ts @@ -118,6 +118,26 @@ export default class GitHubClient { return false; } + async getMasterLocaleFromRepo(repo: string): Promise { + try { + const response = await this.httpClient.get( + `https://raw.githubusercontent.com/${this.username}/${repo}/main/stack/locales/master-locale.json`, + ); + + if (response.data) { + const localeData = response.data; + const localeKey = Object.keys(localeData)[0]; + if (localeKey && localeData[localeKey]?.code) { + return localeData[localeKey].code; + } + } + } catch (error) { + console.log('Could not fetch master locale from repository', error); + } + + return null; + } + async getLatestTarballUrl(repo: string) { try { const response = await this.httpClient.get(`${this.gitHubRepoUrl}/${repo}/releases/latest`); diff --git a/packages/contentstack-seed/src/seed/importer.ts b/packages/contentstack-seed/src/seed/importer.ts index ddeece2f4..e3b8b2b9b 100644 --- a/packages/contentstack-seed/src/seed/importer.ts +++ b/packages/contentstack-seed/src/seed/importer.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import * as process from 'process'; import * as path from 'path'; import ImportCommand from '@contentstack/cli-cm-import'; @@ -16,7 +17,13 @@ export interface ImporterOptions { } export async function run(options: ImporterOptions) { - const importPath = pathValidator(path.resolve(sanitizePath(options.tmpPath), STACK_FOLDER)); + const tmpPathResolved = path.resolve(sanitizePath(options.tmpPath)); + const stackPath = path.join(tmpPathResolved, STACK_FOLDER); + + // Support both structures: repo with stack/ folder (per docs) or content at root + const importPath = fs.existsSync(stackPath) + ? pathValidator(stackPath) + : pathValidator(tmpPathResolved); const args = options.alias ? ['-k', options.api_key, '-d', importPath, '--alias', options.alias!] diff --git a/packages/contentstack-seed/src/seed/index.ts b/packages/contentstack-seed/src/seed/index.ts index 4223acb0a..31e6fb9aa 100644 --- a/packages/contentstack-seed/src/seed/index.ts +++ b/packages/contentstack-seed/src/seed/index.ts @@ -27,7 +27,7 @@ export interface ContentModelSeederOptions { stackUid: string | undefined; stackName: string | undefined; fetchLimit: string | undefined; - skipStackConfirmation: string | undefined; + skipStackConfirmation: boolean | undefined; isAuthenticated: boolean | false; managementToken?: string | undefined; alias?: string | undefined; @@ -181,52 +181,24 @@ export default class ContentModelSeeder { } async shouldProceed(api_key: string) { - let count; const stack_details = await this.csClient.getStack(api_key); - if(this.options.master_locale != stack_details.master_locale){ - cliux.print(`Compass app requires the master locale to be set to English (en).`,{ - color: "yellow", - bold: true, - }); - return false; - } - const managementBody = { - "name":"Checking roles for creating management token", - "description":"This is a compass app management token.", - "scope":[ - { - "module":"content_type", - "acl":{ - "read":true, - "write":true - } - }, - { - "module":"branch", - "branches":[ - "main" - ], - "acl":{ - "read":true - } - } - ], - "expires_on": "3000-01-01", - "is_email_notification_enabled":false - } - let managementTokenResult = await this.csClient.createManagementToken(api_key, this.managementToken, managementBody); - if(managementTokenResult?.response_code == "161" || managementTokenResult?.response_code == "401"){ + const repoMasterLocale = await this.ghClient.getMasterLocaleFromRepo(this.ghRepo as string); + const expectedLocale = repoMasterLocale || this.options.master_locale || ENGLISH_LOCALE; + + if (stack_details.master_locale !== expectedLocale) { cliux.print( - `Info: Failed to generate a management token.\nNote: Management token is not available in your plan. Please contact the admin for support.`, + `Repository '${this.ghRepo}' requires the master locale to be set to '${expectedLocale}', but your stack has '${stack_details.master_locale}'.`, { - color: 'red', + color: 'yellow', + bold: true, }, ); return false; - } - count = await this.csClient.getContentTypeCount(api_key, this.managementToken); + } + + const count = await this.csClient.getContentTypeCount(api_key, this.managementToken); - if (count > 0 && this._options.skipStackConfirmation !== 'yes') { + if (count > 0 && !this._options.skipStackConfirmation) { const proceed = await inquireProceed(); if (!proceed) { diff --git a/packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts b/packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts index 889d133f3..d5a3a9811 100644 --- a/packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts +++ b/packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts @@ -224,7 +224,7 @@ describe('SeedCommand', () => { 'stack-api-key': undefined, 'stack-name': undefined, 'fetch-limit': undefined, - yes: 'yes', + yes: true, alias: undefined, locale: undefined, }; @@ -239,7 +239,7 @@ describe('SeedCommand', () => { expect(ContentModelSeeder).toHaveBeenCalledWith( expect.objectContaining({ - skipStackConfirmation: 'yes', + skipStackConfirmation: true, }), ); }); @@ -251,7 +251,7 @@ describe('SeedCommand', () => { 'stack-api-key': undefined, 'stack-name': 'My Stack', 'fetch-limit': '100', - yes: 'yes', + yes: true, alias: 'my-alias', locale: 'fr-fr', }; @@ -273,7 +273,7 @@ describe('SeedCommand', () => { stackUid: undefined, stackName: 'My Stack', fetchLimit: '100', - skipStackConfirmation: 'yes', + skipStackConfirmation: true, isAuthenticated: true, alias: 'my-alias', master_locale: 'fr-fr', @@ -349,7 +349,7 @@ describe('SeedCommand', () => { it('should have correct usage', () => { expect(SeedCommand.usage).toBe( - 'cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [--yes ] [--alias ] [--locale ]', + 'cm:stacks:seed [--repo ] [--org ] [--stack-api-key ] [--stack-name ] [-y] [--alias ] [--locale ]', ); }); diff --git a/packages/contentstack-seed/test/seed/importer.test.ts b/packages/contentstack-seed/test/seed/importer.test.ts index 73291a5b2..039100943 100644 --- a/packages/contentstack-seed/test/seed/importer.test.ts +++ b/packages/contentstack-seed/test/seed/importer.test.ts @@ -13,6 +13,7 @@ jest.mock('@contentstack/cli-utilities', () => { jest.mock('@contentstack/cli-cm-import'); jest.mock('@contentstack/cli-utilities'); +import * as fs from 'fs'; import * as importer from '../../src/seed/importer'; import ImportCommand from '@contentstack/cli-cm-import'; import * as path from 'node:path'; @@ -37,6 +38,11 @@ describe('Importer', () => { jest.spyOn(cliUtilities, 'pathValidator').mockImplementation((p: any) => p); jest.spyOn(cliUtilities, 'sanitizePath').mockImplementation((p: any) => p); (ImportCommand.run as jest.Mock) = jest.fn().mockResolvedValue(undefined); + // Mock fs.existsSync: stack folder exists (standard repo structure) + jest.spyOn(fs, 'existsSync').mockImplementation((checkPath: fs.PathLike) => { + const p = typeof checkPath === 'string' ? checkPath : checkPath.toString(); + return p.endsWith('stack') || p.includes(path.sep + 'stack'); + }); }); describe('run', () => { @@ -176,5 +182,21 @@ describe('Importer', () => { const expectedPath = path.resolve(mockOptions.tmpPath, 'stack'); expect(cliUtilities.pathValidator).toHaveBeenCalledWith(expectedPath); }); + + it('should use tmpPath when stack folder does not exist (content at root)', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(false); + + await importer.run(mockOptions); + + const expectedPath = path.resolve(mockOptions.tmpPath); + expect(cliUtilities.pathValidator).toHaveBeenCalledWith(expectedPath); + expect(ImportCommand.run).toHaveBeenCalledWith([ + '-k', + mockOptions.api_key, + '-d', + expectedPath, + '--skip-audit', + ]); + }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d371a2179..280f71a78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,7 +103,7 @@ importers: packages/contentstack-bootstrap: dependencies: '@contentstack/cli-cm-seed': - specifier: ~2.0.0-beta.9 + specifier: ~2.0.0-beta.10 version: link:../contentstack-seed '@contentstack/cli-command': specifier: ~2.0.0-beta @@ -10597,7 +10597,7 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint-config-xo-space: 0.35.0(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1) eslint-plugin-perfectionist: 2.11.0(eslint@8.57.1)(typescript@5.9.3) @@ -10659,7 +10659,7 @@ snapshots: eslint-config-xo: 0.49.0(eslint@8.57.1) eslint-config-xo-space: 0.35.0(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsdoc: 50.8.0(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 17.24.0(eslint@8.57.1)(typescript@5.9.3) @@ -10711,7 +10711,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -10816,7 +10816,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10874,7 +10874,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9