Skip to content

Commit 45fbe3e

Browse files
Merge pull request #11 from contentstack/fix/dx-4442
Fixed seed command for existing stack
2 parents 79ad703 + 34fa65a commit 45fbe3e

File tree

11 files changed

+93
-70
lines changed

11 files changed

+93
-70
lines changed

packages/contentstack-bootstrap/src/bootstrap/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface SeedParams {
2828
stackAPIKey?: string;
2929
org?: string;
3030
stackName?: string;
31-
yes?: string;
31+
yes?: boolean;
3232
managementTokenAlias?: string | undefined;
3333
managementToken?: string | undefined;
3434
}
@@ -95,7 +95,7 @@ export default class Bootstrap {
9595
cmd.push('-n', this.options.seedParams.stackName);
9696
}
9797
if (this.options.seedParams.yes) {
98-
cmd.push('-y', this.options.seedParams.yes);
98+
cmd.push('-y');
9999
}
100100
if (this.options.seedParams.managementTokenAlias) {
101101
cmd.push('--alias', this.options.seedParams.managementTokenAlias);

packages/contentstack-bootstrap/src/bootstrap/utils.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ export const setupEnvironments = async (
4040
if (!managementToken) {
4141
const managementBody = {
4242
token: {
43-
name: 'sample app',
44-
description: 'This is a sample management token.',
43+
name: 'Compass Starter App',
44+
description: 'This is a compass starter app management token.',
4545
scope: [
4646
{
47-
module: 'content_type',
47+
module: '$all',
4848
acl: {
4949
read: true,
5050
write: true,
@@ -58,8 +58,9 @@ export const setupEnvironments = async (
5858
},
5959
},
6060
],
61-
expires_on: '3000-01-01',
61+
expires_on: null,
6262
is_email_notification_enabled: false,
63+
rate_limit_enabled: false,
6364
},
6465
};
6566
managementTokenResult = await managementAPIClient
@@ -313,7 +314,7 @@ const envFileHandler = async (
313314
}CONTENTSTACK_ENVIRONMENT=${environmentVariables.environment}${!isUSRegion && !customHost ? '\nCONTENTSTACK_REGION=' + region.name : ''
314315
}\nCONTENTSTACK_LIVE_PREVIEW=${livePreviewEnabled}\nCONTENTSTACK_LIVE_EDIT_TAGS=false\nCONTENTSTACK_API_HOST=${customHost ? customHost : managementAPIHost
315316
}${!isUSRegion && !customHost ? '\nCONTENTSTACK_REGION=' + region.name : ''
316-
}\nCONTENTSTACK_APP_HOST=${appHost}\nCONTENTSTACK_MANAGEMENT_TOKEN=${managementTokenResult.uid
317+
}\nCONTENTSTACK_APP_HOST=${appHost}\nCONTENTSTACK_MANAGEMENT_TOKEN=${managementTokenResult.token
317318
}\nCONTENTSTACK_HOST=${cdnHost}`;
318319
result = await writeEnvFile(content, filePath);
319320
break;

packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default class BootstrapCommand extends Command {
6666
required: false,
6767
exclusive: ['stack-api-key'],
6868
}),
69-
yes: flags.string({
69+
yes: flags.boolean({
7070
description: '[Optional] Skip stack confirmation',
7171
char: 'y',
7272
required: false,
@@ -123,7 +123,7 @@ export default class BootstrapCommand extends Command {
123123
});
124124
}
125125

126-
const yes = bootstrapCommandFlags.yes as string;
126+
const yes = bootstrapCommandFlags.yes as boolean;
127127

128128
const appConfig: AppConfig = getAppLevelConfigByName(selectedAppName || selectedApp.configKey);
129129
const master_locale = appConfig.master_locale || DEFAULT_MASTER_LOCALE;
@@ -147,7 +147,7 @@ export default class BootstrapCommand extends Command {
147147
if (stackAPIKey) seedParams.stackAPIKey = stackAPIKey;
148148
if (org) seedParams.org = org;
149149
if (stackName) seedParams.stackName = stackName;
150-
if (yes) seedParams.yes = yes;
150+
if (yes) seedParams.yes = true;
151151
if (managementTokenAlias) {
152152
seedParams.managementTokenAlias = managementTokenAlias;
153153
const listOfTokens = configHandler.get('tokens');

packages/contentstack-seed/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,22 @@ To import content to your stack, you can choose from the following two sources:
1010
<!-- usagestop -->
1111
## Commands
1212
<!-- commands -->
13-
* [`csdx cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [--yes <value>] [--alias <value>] [--locale <value>]`](#csdx-cmstacksseed---repo-value---org-value---stack-api-key-value---stack-name-value---yes-value---alias-value---locale-value)
13+
* [`csdx cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [-y] [--alias <value>] [--locale <value>]`](#csdx-cmstacksseed---repo-value---org-value---stack-api-key-value---stack-name-value---y---alias-value---locale-value)
1414

15-
## `csdx cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [--yes <value>] [--alias <value>] [--locale <value>]`
15+
## `csdx cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [-y] [--alias <value>] [--locale <value>]`
1616

1717
Create a stack from existing content types, entries, assets, etc
1818

1919
```
2020
USAGE
21-
$ csdx cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [--yes
22-
<value>] [--alias <value>] [--locale <value>]
21+
$ csdx cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [-y]
22+
[--alias <value>] [--locale <value>]
2323
2424
FLAGS
2525
-a, --alias=<value> Alias of the management token
2626
-k, --stack-api-key=<value> Provide stack API key to seed content to
2727
-n, --stack-name=<value> Name of a new stack that needs to be created.
28-
-y, --yes=<value> [Optional] Skip the stack confirmation.
28+
-y, --yes [Optional] Skip the stack confirmation.
2929
--org=<value> Provide Organization UID to create a new stack
3030
--repo=<value> GitHub organization name or GitHub user name/repository name.
3131

packages/contentstack-seed/src/commands/cm/stacks/seed.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default class SeedCommand extends Command {
1414
];
1515

1616
static usage =
17-
'cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [--yes <value>] [--alias <value>] [--locale <value>]';
17+
'cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [-y] [--alias <value>] [--locale <value>]';
1818

1919
static flags: FlagInput = {
2020
repo: flags.string({
@@ -49,7 +49,7 @@ export default class SeedCommand extends Command {
4949
required: false,
5050
hidden: true,
5151
}),
52-
yes: flags.string({
52+
yes: flags.boolean({
5353
char: 'y',
5454
required: false,
5555
description: '[Optional] Skip the stack confirmation.',
@@ -60,6 +60,7 @@ export default class SeedCommand extends Command {
6060
}),
6161
locale: flags.string({
6262
description: 'Master Locale of the stack',
63+
default: 'en-us',
6364
hidden: true,
6465
}),
6566
};
@@ -84,7 +85,7 @@ export default class SeedCommand extends Command {
8485
stackUid: seedFlags['stack-api-key'],
8586
stackName: seedFlags['stack-name'],
8687
fetchLimit: seedFlags['fetch-limit'],
87-
skipStackConfirmation: seedFlags['yes'],
88+
skipStackConfirmation: seedFlags.yes,
8889
isAuthenticated: isAuthenticated(),
8990
alias: managementTokenAlias,
9091
master_locale: seedFlags['locale'],

packages/contentstack-seed/src/seed/github/client.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ export default class GitHubClient {
118118
return false;
119119
}
120120

121+
async getMasterLocaleFromRepo(repo: string): Promise<string | null> {
122+
try {
123+
const response = await this.httpClient.get(
124+
`https://raw.githubusercontent.com/${this.username}/${repo}/main/stack/locales/master-locale.json`,
125+
);
126+
127+
if (response.data) {
128+
const localeData = response.data;
129+
const localeKey = Object.keys(localeData)[0];
130+
if (localeKey && localeData[localeKey]?.code) {
131+
return localeData[localeKey].code;
132+
}
133+
}
134+
} catch (error) {
135+
console.log('Could not fetch master locale from repository', error);
136+
}
137+
138+
return null;
139+
}
140+
121141
async getLatestTarballUrl(repo: string) {
122142
try {
123143
const response = await this.httpClient.get(`${this.gitHubRepoUrl}/${repo}/releases/latest`);

packages/contentstack-seed/src/seed/importer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as fs from 'fs';
12
import * as process from 'process';
23
import * as path from 'path';
34
import ImportCommand from '@contentstack/cli-cm-import';
@@ -16,7 +17,13 @@ export interface ImporterOptions {
1617
}
1718

1819
export async function run(options: ImporterOptions) {
19-
const importPath = pathValidator(path.resolve(sanitizePath(options.tmpPath), STACK_FOLDER));
20+
const tmpPathResolved = path.resolve(sanitizePath(options.tmpPath));
21+
const stackPath = path.join(tmpPathResolved, STACK_FOLDER);
22+
23+
// Support both structures: repo with stack/ folder (per docs) or content at root
24+
const importPath = fs.existsSync(stackPath)
25+
? pathValidator(stackPath)
26+
: pathValidator(tmpPathResolved);
2027

2128
const args = options.alias
2229
? ['-k', options.api_key, '-d', importPath, '--alias', options.alias!]

packages/contentstack-seed/src/seed/index.ts

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface ContentModelSeederOptions {
2727
stackUid: string | undefined;
2828
stackName: string | undefined;
2929
fetchLimit: string | undefined;
30-
skipStackConfirmation: string | undefined;
30+
skipStackConfirmation: boolean | undefined;
3131
isAuthenticated: boolean | false;
3232
managementToken?: string | undefined;
3333
alias?: string | undefined;
@@ -181,52 +181,24 @@ export default class ContentModelSeeder {
181181
}
182182

183183
async shouldProceed(api_key: string) {
184-
let count;
185184
const stack_details = await this.csClient.getStack(api_key);
186-
if(this.options.master_locale != stack_details.master_locale){
187-
cliux.print(`Compass app requires the master locale to be set to English (en).`,{
188-
color: "yellow",
189-
bold: true,
190-
});
191-
return false;
192-
}
193-
const managementBody = {
194-
"name":"Checking roles for creating management token",
195-
"description":"This is a compass app management token.",
196-
"scope":[
197-
{
198-
"module":"content_type",
199-
"acl":{
200-
"read":true,
201-
"write":true
202-
}
203-
},
204-
{
205-
"module":"branch",
206-
"branches":[
207-
"main"
208-
],
209-
"acl":{
210-
"read":true
211-
}
212-
}
213-
],
214-
"expires_on": "3000-01-01",
215-
"is_email_notification_enabled":false
216-
}
217-
let managementTokenResult = await this.csClient.createManagementToken(api_key, this.managementToken, managementBody);
218-
if(managementTokenResult?.response_code == "161" || managementTokenResult?.response_code == "401"){
185+
const repoMasterLocale = await this.ghClient.getMasterLocaleFromRepo(this.ghRepo as string);
186+
const expectedLocale = repoMasterLocale || this.options.master_locale || ENGLISH_LOCALE;
187+
188+
if (stack_details.master_locale !== expectedLocale) {
219189
cliux.print(
220-
`Info: Failed to generate a management token.\nNote: Management token is not available in your plan. Please contact the admin for support.`,
190+
`Repository '${this.ghRepo}' requires the master locale to be set to '${expectedLocale}', but your stack has '${stack_details.master_locale}'.`,
221191
{
222-
color: 'red',
192+
color: 'yellow',
193+
bold: true,
223194
},
224195
);
225196
return false;
226-
}
227-
count = await this.csClient.getContentTypeCount(api_key, this.managementToken);
197+
}
198+
199+
const count = await this.csClient.getContentTypeCount(api_key, this.managementToken);
228200

229-
if (count > 0 && this._options.skipStackConfirmation !== 'yes') {
201+
if (count > 0 && !this._options.skipStackConfirmation) {
230202
const proceed = await inquireProceed();
231203

232204
if (!proceed) {

packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ describe('SeedCommand', () => {
224224
'stack-api-key': undefined,
225225
'stack-name': undefined,
226226
'fetch-limit': undefined,
227-
yes: 'yes',
227+
yes: true,
228228
alias: undefined,
229229
locale: undefined,
230230
};
@@ -239,7 +239,7 @@ describe('SeedCommand', () => {
239239

240240
expect(ContentModelSeeder).toHaveBeenCalledWith(
241241
expect.objectContaining({
242-
skipStackConfirmation: 'yes',
242+
skipStackConfirmation: true,
243243
}),
244244
);
245245
});
@@ -251,7 +251,7 @@ describe('SeedCommand', () => {
251251
'stack-api-key': undefined,
252252
'stack-name': 'My Stack',
253253
'fetch-limit': '100',
254-
yes: 'yes',
254+
yes: true,
255255
alias: 'my-alias',
256256
locale: 'fr-fr',
257257
};
@@ -273,7 +273,7 @@ describe('SeedCommand', () => {
273273
stackUid: undefined,
274274
stackName: 'My Stack',
275275
fetchLimit: '100',
276-
skipStackConfirmation: 'yes',
276+
skipStackConfirmation: true,
277277
isAuthenticated: true,
278278
alias: 'my-alias',
279279
master_locale: 'fr-fr',
@@ -349,7 +349,7 @@ describe('SeedCommand', () => {
349349

350350
it('should have correct usage', () => {
351351
expect(SeedCommand.usage).toBe(
352-
'cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [--yes <value>] [--alias <value>] [--locale <value>]',
352+
'cm:stacks:seed [--repo <value>] [--org <value>] [--stack-api-key <value>] [--stack-name <value>] [-y] [--alias <value>] [--locale <value>]',
353353
);
354354
});
355355

packages/contentstack-seed/test/seed/importer.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jest.mock('@contentstack/cli-utilities', () => {
1313
jest.mock('@contentstack/cli-cm-import');
1414
jest.mock('@contentstack/cli-utilities');
1515

16+
import * as fs from 'fs';
1617
import * as importer from '../../src/seed/importer';
1718
import ImportCommand from '@contentstack/cli-cm-import';
1819
import * as path from 'node:path';
@@ -37,6 +38,11 @@ describe('Importer', () => {
3738
jest.spyOn(cliUtilities, 'pathValidator').mockImplementation((p: any) => p);
3839
jest.spyOn(cliUtilities, 'sanitizePath').mockImplementation((p: any) => p);
3940
(ImportCommand.run as jest.Mock) = jest.fn().mockResolvedValue(undefined);
41+
// Mock fs.existsSync: stack folder exists (standard repo structure)
42+
jest.spyOn(fs, 'existsSync').mockImplementation((checkPath: fs.PathLike) => {
43+
const p = typeof checkPath === 'string' ? checkPath : checkPath.toString();
44+
return p.endsWith('stack') || p.includes(path.sep + 'stack');
45+
});
4046
});
4147

4248
describe('run', () => {
@@ -176,5 +182,21 @@ describe('Importer', () => {
176182
const expectedPath = path.resolve(mockOptions.tmpPath, 'stack');
177183
expect(cliUtilities.pathValidator).toHaveBeenCalledWith(expectedPath);
178184
});
185+
186+
it('should use tmpPath when stack folder does not exist (content at root)', async () => {
187+
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
188+
189+
await importer.run(mockOptions);
190+
191+
const expectedPath = path.resolve(mockOptions.tmpPath);
192+
expect(cliUtilities.pathValidator).toHaveBeenCalledWith(expectedPath);
193+
expect(ImportCommand.run).toHaveBeenCalledWith([
194+
'-k',
195+
mockOptions.api_key,
196+
'-d',
197+
expectedPath,
198+
'--skip-audit',
199+
]);
200+
});
179201
});
180202
});

0 commit comments

Comments
 (0)