@contentstack/cli-variants 1.3.3 → 2.0.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/lib/export/attributes.d.ts +2 -2
  2. package/lib/export/attributes.js +51 -23
  3. package/lib/export/audiences.d.ts +2 -2
  4. package/lib/export/audiences.js +50 -24
  5. package/lib/export/events.d.ts +2 -2
  6. package/lib/export/events.js +52 -24
  7. package/lib/export/experiences.js +87 -54
  8. package/lib/export/projects.d.ts +3 -2
  9. package/lib/export/projects.js +55 -63
  10. package/lib/export/variant-entries.d.ts +19 -0
  11. package/lib/export/variant-entries.js +76 -1
  12. package/lib/import/attribute.d.ts +2 -0
  13. package/lib/import/attribute.js +83 -37
  14. package/lib/import/audiences.d.ts +2 -0
  15. package/lib/import/audiences.js +85 -41
  16. package/lib/import/events.d.ts +3 -1
  17. package/lib/import/events.js +86 -30
  18. package/lib/import/experiences.d.ts +2 -0
  19. package/lib/import/experiences.js +93 -39
  20. package/lib/import/project.d.ts +2 -0
  21. package/lib/import/project.js +81 -22
  22. package/lib/import/variant-entries.d.ts +10 -0
  23. package/lib/import/variant-entries.js +132 -47
  24. package/lib/index.d.ts +1 -0
  25. package/lib/index.js +1 -0
  26. package/lib/types/export-config.d.ts +0 -2
  27. package/lib/types/import-config.d.ts +0 -1
  28. package/lib/types/utils.d.ts +1 -1
  29. package/lib/utils/constants.d.ts +91 -0
  30. package/lib/utils/constants.js +93 -0
  31. package/lib/utils/personalization-api-adapter.d.ts +34 -1
  32. package/lib/utils/personalization-api-adapter.js +171 -44
  33. package/lib/utils/variant-api-adapter.d.ts +28 -1
  34. package/lib/utils/variant-api-adapter.js +75 -0
  35. package/package.json +1 -1
  36. package/src/export/attributes.ts +84 -34
  37. package/src/export/audiences.ts +87 -41
  38. package/src/export/events.ts +84 -41
  39. package/src/export/experiences.ts +155 -83
  40. package/src/export/projects.ts +71 -39
  41. package/src/export/variant-entries.ts +136 -12
  42. package/src/import/attribute.ts +105 -49
  43. package/src/import/audiences.ts +110 -54
  44. package/src/import/events.ts +104 -41
  45. package/src/import/experiences.ts +140 -62
  46. package/src/import/project.ts +108 -38
  47. package/src/import/variant-entries.ts +179 -65
  48. package/src/index.ts +2 -1
  49. package/src/types/export-config.ts +0 -2
  50. package/src/types/import-config.ts +0 -1
  51. package/src/types/utils.ts +1 -1
  52. package/src/utils/constants.ts +98 -0
  53. package/src/utils/personalization-api-adapter.ts +202 -66
  54. package/src/utils/variant-api-adapter.ts +82 -1
  55. package/tsconfig.json +1 -1
@@ -11,6 +11,7 @@ import {
11
11
  CreateExperienceInput,
12
12
  CreateExperienceVersionInput,
13
13
  } from '../types';
14
+ import { PROCESS_NAMES, MODULE_CONTEXTS } from '../utils/constants';
14
15
 
15
16
  export default class Experiences extends PersonalizationAdapter<ImportConfig> {
16
17
  private createdCTs: string[];
@@ -40,9 +41,10 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
40
41
  private personalizeConfig: ImportConfig['modules']['personalize'];
41
42
  private audienceConfig: ImportConfig['modules']['personalize']['audiences'];
42
43
  private experienceConfig: ImportConfig['modules']['personalize']['experiences'];
44
+ private experiences: ExperienceStruct[];
43
45
 
44
46
  constructor(public readonly config: ImportConfig) {
45
- const conf: APIConfig = {
47
+ const conf: APIConfig = {
46
48
  config,
47
49
  baseURL: config.modules.personalize.baseURL[config.region.name],
48
50
  headers: { 'X-Project-Uid': config.modules.personalize.project_id },
@@ -52,7 +54,7 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
52
54
  },
53
55
  };
54
56
  super(Object.assign(config, conf));
55
-
57
+
56
58
  this.personalizeConfig = this.config.modules.personalize;
57
59
  this.experiencesDirPath = resolve(
58
60
  sanitizePath(this.config.data),
@@ -101,33 +103,55 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
101
103
  this.createdCTs = [];
102
104
  this.audiencesUid = (fsUtil.readFile(this.audiencesMapperPath, true) as Record<string, string>) || {};
103
105
  this.eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record<string, string>) || {};
104
- this.config.context.module = 'experiences';
106
+ this.config.context.module = MODULE_CONTEXTS.EXPERIENCES;
107
+ this.experiences = [];
105
108
  }
106
109
 
107
110
  /**
108
111
  * The function asynchronously imports experiences from a JSON file and creates them in the system.
109
112
  */
110
- async import() {
111
- await this.init();
112
- await fsUtil.makeDirectory(this.expMapperDirPath);
113
- log.debug(`Created mapper directory: ${this.expMapperDirPath}`, this.config.context);
114
-
115
- if (existsSync(this.experiencesPath)) {
116
- log.debug(`Loading experiences from: ${this.experiencesPath}`, this.config.context);
117
-
118
- try {
119
- const experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[];
120
- log.info(`Found ${experiences.length} experiences to import`, this.config.context);
121
-
122
- for (const experience of experiences) {
123
- const { uid, ...restExperienceData } = experience;
124
- log.debug(`Processing experience: ${uid}`, this.config.context);
125
-
126
- //check whether reference audience exists or not that referenced in variations having __type equal to AudienceBasedVariation & targeting
127
- let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, this.audiencesUid);
128
- //check whether events exists or not that referenced in metrics
129
- experienceReqObj = lookUpEvents(experienceReqObj, this.eventsUid);
113
+ async import() {
114
+ try {
115
+ log.debug('Starting experiences import...', this.config.context);
116
+
117
+ const [canImport, experiencesCount] = await this.analyzeExperiences();
118
+ if (!canImport) {
119
+ log.info('No experiences found to import', this.config.context);
120
+ // Still need to mark as complete for parent progress
121
+ if (this.parentProgressManager) {
122
+ this.parentProgressManager.tick(true, 'experiences module (no data)', null, PROCESS_NAMES.EXPERIENCES);
123
+ }
124
+ return;
125
+ }
126
+
127
+ // If we have a parent progress manager, use it as a sub-module
128
+ // Otherwise create our own simple progress manager
129
+ let progress;
130
+ if (this.parentProgressManager) {
131
+ progress = this.parentProgressManager;
132
+ log.debug('Using parent progress manager for experiences import', this.config.context);
133
+ this.parentProgressManager.updateProcessTotal(PROCESS_NAMES.EXPERIENCES, experiencesCount);
134
+ } else {
135
+ progress = this.createSimpleProgress(PROCESS_NAMES.EXPERIENCES, experiencesCount);
136
+ log.debug('Created standalone progress manager for experiences import', this.config.context);
137
+ }
138
+
139
+ await this.init();
140
+ await fsUtil.makeDirectory(this.expMapperDirPath);
141
+ log.debug(`Created mapper directory: ${this.expMapperDirPath}`, this.config.context);
142
+
143
+ log.info(`Processing ${experiencesCount} experiences for import`, this.config.context);
130
144
 
145
+ for (const experience of this.experiences) {
146
+ const { uid, ...restExperienceData } = experience;
147
+ log.debug(`Processing experience: ${uid}`, this.config.context);
148
+
149
+ //check whether reference audience exists or not that referenced in variations having __type equal to AudienceBasedVariation & targeting
150
+ let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, this.audiencesUid);
151
+ //check whether events exists or not that referenced in metrics
152
+ experienceReqObj = lookUpEvents(experienceReqObj, this.eventsUid);
153
+
154
+ try {
131
155
  const expRes = (await this.createExperience(experienceReqObj)) as ExperienceStruct;
132
156
  //map old experience uid to new experience uid
133
157
  this.experiencesUidMapper[uid] = expRes?.uid ?? '';
@@ -139,45 +163,90 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
139
163
  } catch (error) {
140
164
  handleAndLogError(error, this.config.context, `Failed to import experience versions for ${expRes.uid}`);
141
165
  }
142
- }
143
-
144
- fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper);
145
- log.success('Experiences created successfully', this.config.context);
146
-
147
- log.info('Validating variant and variant group creation',this.config.context);
148
- this.pendingVariantAndVariantGrpForExperience = values(cloneDeep(this.experiencesUidMapper));
149
- const jobRes = await this.validateVariantGroupAndVariantsCreated();
150
- fsUtil.writeFile(this.cmsVariantPath, this.cmsVariants);
151
- fsUtil.writeFile(this.cmsVariantGroupPath, this.cmsVariantGroups);
152
-
153
- if (jobRes) {
154
- log.success('Variant and variant groups created successfully', this.config.context);
155
- } else {
156
- log.error('Failed to create variants and variant groups', this.config.context);
157
- this.personalizeConfig.importData = false;
158
- }
159
166
 
160
- if (this.personalizeConfig.importData) {
161
- log.info('Attaching content types to experiences', this.config.context);
162
- await this.attachCTsInExperience();
163
- log.success('Content types attached to experiences successfully', this.config.context);
167
+ this.updateProgress(true, `experience: ${experience.name || uid}`, undefined, PROCESS_NAMES.EXPERIENCES);
168
+ log.debug(`Successfully processed experience: ${uid}`, this.config.context);
169
+ } catch (error) {
170
+ this.updateProgress(
171
+ false,
172
+ `experience: ${experience.name || uid}`,
173
+ (error as any)?.message,
174
+ PROCESS_NAMES.EXPERIENCES,
175
+ );
176
+ handleAndLogError(error, this.config.context, `Failed to create experience: ${uid}`);
164
177
  }
178
+ }
165
179
 
166
- await this.createVariantIdMapper();
167
- } catch (error) {
168
- handleAndLogError(error, this.config.context);
180
+ fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper);
181
+ log.success('Experiences created successfully', this.config.context);
182
+
183
+ log.info('Validating variant and variant group creation', this.config.context);
184
+ this.pendingVariantAndVariantGrpForExperience = values(cloneDeep(this.experiencesUidMapper));
185
+ const jobRes = await this.validateVariantGroupAndVariantsCreated();
186
+ fsUtil.writeFile(this.cmsVariantPath, this.cmsVariants);
187
+ fsUtil.writeFile(this.cmsVariantGroupPath, this.cmsVariantGroups);
188
+
189
+ if (jobRes) {
190
+ log.success('Variant and variant groups created successfully', this.config.context);
191
+ } else {
192
+ log.error('Failed to create variants and variant groups', this.config.context);
193
+ this.personalizeConfig.importData = false;
169
194
  }
170
- } else {
171
- log.warn(`Experiences file not found: ${this.experiencesPath}`, this.config.context);
195
+
196
+ if (this.personalizeConfig.importData) {
197
+ log.info('Attaching content types to experiences', this.config.context);
198
+ await this.attachCTsInExperience();
199
+ log.success('Content types attached to experiences successfully', this.config.context);
200
+ }
201
+
202
+ await this.createVariantIdMapper();
203
+
204
+ // Only complete progress if we own the progress manager (no parent)
205
+ if (!this.parentProgressManager) {
206
+ this.completeProgress(true);
207
+ }
208
+
209
+ log.success(
210
+ `Experiences imported successfully! Total experiences: ${experiencesCount} - personalization enabled`,
211
+ this.config.context,
212
+ );
213
+ } catch (error) {
214
+ if (!this.parentProgressManager) {
215
+ this.completeProgress(false, (error as any)?.message || 'Experiences import failed');
216
+ }
217
+ handleAndLogError(error, this.config.context);
218
+ throw error;
172
219
  }
173
220
  }
174
221
 
222
+ private async analyzeExperiences(): Promise<[boolean, number]> {
223
+ return this.withLoadingSpinner('EXPERIENCES: Analyzing import data...', async () => {
224
+ log.debug(`Checking for experiences file: ${this.experiencesPath}`, this.config.context);
225
+
226
+ if (!existsSync(this.experiencesPath)) {
227
+ log.warn(`Experiences file not found: ${this.experiencesPath}`, this.config.context);
228
+ return [false, 0];
229
+ }
230
+
231
+ this.experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[];
232
+ const experiencesCount = this.experiences?.length || 0;
233
+
234
+ if (experiencesCount < 1) {
235
+ log.warn('No experiences found in file', this.config.context);
236
+ return [false, 0];
237
+ }
238
+
239
+ log.debug(`Found ${experiencesCount} experiences to import`, this.config.context);
240
+ return [true, experiencesCount];
241
+ });
242
+ }
243
+
175
244
  /**
176
245
  * function import experience versions from a JSON file and creates them in the project.
177
246
  */
178
247
  async importExperienceVersions(experience: ExperienceStruct, oldExperienceUid: string) {
179
248
  log.debug(`Importing versions for experience: ${oldExperienceUid}`, this.config.context);
180
-
249
+
181
250
  const versionsPath = resolve(
182
251
  sanitizePath(this.experiencesDirPath),
183
252
  'versions',
@@ -191,7 +260,7 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
191
260
 
192
261
  const versions = fsUtil.readFile(versionsPath, true) as ExperienceStruct[];
193
262
  log.debug(`Found ${versions.length} versions for experience: ${oldExperienceUid}`, this.config.context);
194
-
263
+
195
264
  const versionMap: Record<string, CreateExperienceVersionInput | undefined> = {
196
265
  ACTIVE: undefined,
197
266
  DRAFT: undefined,
@@ -258,8 +327,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
258
327
  * @returns
259
328
  */
260
329
  async validateVariantGroupAndVariantsCreated(retryCount = 0): Promise<any> {
261
- log.debug(`Validating variant groups and variants creation - attempt ${retryCount + 1}/${this.maxValidateRetry}`, this.config.context);
262
-
330
+ log.debug(
331
+ `Validating variant groups and variants creation - attempt ${retryCount + 1}/${this.maxValidateRetry}`,
332
+ this.config.context,
333
+ );
334
+
263
335
  try {
264
336
  const promises = this.pendingVariantAndVariantGrpForExperience.map(async (expUid) => {
265
337
  log.debug(`Checking experience: ${expUid}`, this.config.context);
@@ -289,7 +361,10 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
289
361
  return this.validateVariantGroupAndVariantsCreated(retryCount);
290
362
  } else {
291
363
  log.error('Personalize job failed to create variants and variant groups', this.config.context);
292
- log.error(`Failed experiences: ${this.pendingVariantAndVariantGrpForExperience.join(', ')}`, this.config.context);
364
+ log.error(
365
+ `Failed experiences: ${this.pendingVariantAndVariantGrpForExperience.join(', ')}`,
366
+ this.config.context,
367
+ );
293
368
  fsUtil.writeFile(this.failedCmsExpPath, this.pendingVariantAndVariantGrpForExperience);
294
369
  return false;
295
370
  }
@@ -305,7 +380,7 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
305
380
 
306
381
  async attachCTsInExperience() {
307
382
  log.debug('Attaching content types to experiences', this.config.context);
308
-
383
+
309
384
  try {
310
385
  // Read the created content types from the file
311
386
  this.createdCTs = fsUtil.readFile(this.cTsSuccessPath, true) as any;
@@ -313,22 +388,25 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
313
388
  log.debug('No Content types created, skipping following process', this.config.context);
314
389
  return;
315
390
  }
316
-
391
+
317
392
  log.debug(`Found ${this.createdCTs.length} created content types`, this.config.context);
318
393
  const experienceCTsMap = fsUtil.readFile(this.experienceCTsPath, true) as Record<string, string[]>;
319
-
394
+
320
395
  return await Promise.allSettled(
321
396
  Object.entries(this.experiencesUidMapper).map(async ([oldExpUid, newExpUid]) => {
322
397
  if (experienceCTsMap[oldExpUid]?.length) {
323
398
  log.debug(`Processing content types for experience: ${oldExpUid} -> ${newExpUid}`, this.config.context);
324
-
399
+
325
400
  // Filter content types that were created
326
401
  const updatedContentTypes = experienceCTsMap[oldExpUid].filter(
327
402
  (ct: any) => this.createdCTs.includes(ct?.uid) && ct.status === 'linked',
328
403
  );
329
-
404
+
330
405
  if (updatedContentTypes?.length) {
331
- log.debug(`Attaching ${updatedContentTypes.length} content types to experience: ${newExpUid}`, this.config.context);
406
+ log.debug(
407
+ `Attaching ${updatedContentTypes.length} content types to experience: ${newExpUid}`,
408
+ this.config.context,
409
+ );
332
410
  const { variant_groups: [variantGroup] = [] } =
333
411
  (await this.getVariantGroup({ experienceUid: newExpUid })) || {};
334
412
  variantGroup.content_types = updatedContentTypes;
@@ -349,11 +427,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
349
427
 
350
428
  async createVariantIdMapper() {
351
429
  log.debug('Creating variant ID mapper', this.config.context);
352
-
430
+
353
431
  try {
354
432
  const experienceVariantIds: any = fsUtil.readFile(this.experienceVariantsIdsPath, true) || [];
355
433
  log.debug(`Found ${experienceVariantIds.length} experience variant IDs to process`, this.config.context);
356
-
434
+
357
435
  const variantUIDMapper: Record<string, string> = {};
358
436
  for (let experienceVariantId of experienceVariantIds) {
359
437
  const [experienceId, variantShortId, oldVariantId] = experienceVariantId.split('-');
@@ -1,12 +1,14 @@
1
1
  import { join, resolve as pResolve } from 'path';
2
2
  import { existsSync, readFileSync } from 'fs';
3
- import { sanitizePath, log } from '@contentstack/cli-utilities';
3
+ import { sanitizePath, log, cliux } from '@contentstack/cli-utilities';
4
4
  import { PersonalizationAdapter, askProjectName, fsUtil } from '../utils';
5
5
  import { APIConfig, CreateProjectInput, ImportConfig, ProjectStruct } from '../types';
6
+ import { PROCESS_NAMES, MODULE_CONTEXTS, IMPORT_PROCESS_STATUS } from '../utils/constants';
6
7
 
7
8
  export default class Project extends PersonalizationAdapter<ImportConfig> {
8
9
  private projectMapperFolderPath: string;
9
-
10
+ private projectsData: CreateProjectInput[];
11
+
10
12
  constructor(public readonly config: ImportConfig) {
11
13
  const conf: APIConfig = {
12
14
  config,
@@ -14,50 +16,56 @@ export default class Project extends PersonalizationAdapter<ImportConfig> {
14
16
  headers: { organization_uid: config.org_uid },
15
17
  };
16
18
  super(Object.assign(config, conf));
17
-
19
+
18
20
  this.projectMapperFolderPath = pResolve(
19
21
  sanitizePath(this.config.backupDir),
20
22
  'mapper',
21
23
  sanitizePath(this.config.modules.personalize.dirName),
22
24
  'projects',
23
25
  );
24
- this.config.context.module = 'project';
26
+ this.config.context.module = MODULE_CONTEXTS.PROJECTS;
27
+ this.projectsData = [];
25
28
  }
26
29
 
27
30
  /**
28
31
  * The function asynchronously imports projects data from a file and creates projects based on the
29
32
  * data.
30
33
  */
31
- async import() {
32
- const personalize = this.config.modules.personalize;
33
- const { dirName, fileName } = personalize.projects;
34
- const projectPath = join(
35
- sanitizePath(this.config.data),
36
- sanitizePath(personalize.dirName),
37
- sanitizePath(dirName),
38
- sanitizePath(fileName),
39
- );
40
-
41
- log.debug(`Checking for project file: ${projectPath}`, this.config.context);
42
-
43
- if (existsSync(projectPath)) {
44
- const projects = JSON.parse(readFileSync(projectPath, 'utf8')) as CreateProjectInput[];
45
- log.debug(`Loaded ${projects?.length || 0} projects from file`, this.config.context);
34
+ async import() {
35
+ try {
36
+ log.debug('Starting personalize project import...', this.config.context);
46
37
 
47
- if (!projects || projects.length < 1) {
48
- this.config.modules.personalize.importData = false; // Stop personalize import if stack not connected to any project
49
- log.warn('No projects found in file', this.config.context);
38
+ const [canImport, projectsCount] = await this.analyzeProjects();
39
+ if (!canImport) {
40
+ log.info('No projects found to import', this.config.context);
41
+ if (this.parentProgressManager) {
42
+ this.parentProgressManager.tick(true, 'projects module (no data)', null, PROCESS_NAMES.PROJECTS);
43
+ }
50
44
  return;
51
45
  }
52
-
46
+
47
+ // Fix 1: Always use parent progress manager when available
48
+ let progress;
49
+ if (this.parentProgressManager) {
50
+ progress = this.parentProgressManager;
51
+ log.debug('Using parent progress manager for projects import', this.config.context);
52
+ // Don't create our own progress - use parent's
53
+ } else {
54
+ progress = this.createSimpleProgress(PROCESS_NAMES.PROJECTS, projectsCount);
55
+ log.debug('Created standalone progress manager for projects import', this.config.context);
56
+ }
57
+
53
58
  await this.init();
54
-
55
- for (const project of projects) {
59
+
60
+ for (const project of this.projectsData) {
61
+ if (!this.parentProgressManager) {
62
+ progress.updateStatus(IMPORT_PROCESS_STATUS[PROCESS_NAMES.PROJECTS].CREATING);
63
+ }
56
64
  log.debug(`Processing project: ${project.name}`, this.config.context);
57
-
65
+
58
66
  const createProject = async (newName: void | string): Promise<ProjectStruct> => {
59
67
  log.debug(`Creating project with name: ${newName || project.name}`, this.config.context);
60
-
68
+
61
69
  return await this.createProject({
62
70
  name: newName || project.name,
63
71
  description: project.description,
@@ -68,26 +76,88 @@ export default class Project extends PersonalizationAdapter<ImportConfig> {
68
76
  error.includes('personalize.PROJECTS.DUPLICATE_NAME')
69
77
  ) {
70
78
  log.warn(`Project name already exists, generating new name`, this.config.context);
79
+
80
+ // Prevent progress bar corruption with clean newlines
81
+ cliux.print('\n');
71
82
  const projectName = await askProjectName('Copy Of ' + (newName || project.name));
83
+ cliux.print('\n');
84
+ if (this.parentProgressManager) {
85
+ this.parentProgressManager.updateStatus(
86
+ IMPORT_PROCESS_STATUS[PROCESS_NAMES.PROJECTS].CREATING,
87
+ PROCESS_NAMES.PROJECTS,
88
+ );
89
+ }
90
+
72
91
  return await createProject(projectName);
73
92
  }
74
93
  throw error;
75
94
  });
76
95
  };
77
96
 
78
- const projectRes = await createProject(this.config.personalizeProjectName);
79
- this.config.modules.personalize.project_id = projectRes.uid;
80
- this.config.modules.personalize.importData = true;
97
+ try {
98
+ const projectRes = await createProject(this.config.personalizeProjectName);
99
+ this.config.modules.personalize.project_id = projectRes.uid;
100
+ this.config.modules.personalize.importData = true;
101
+
102
+ await fsUtil.makeDirectory(this.projectMapperFolderPath);
103
+ fsUtil.writeFile(pResolve(sanitizePath(this.projectMapperFolderPath), 'projects.json'), projectRes);
81
104
 
82
- await fsUtil.makeDirectory(this.projectMapperFolderPath);
83
- fsUtil.writeFile(pResolve(sanitizePath(this.projectMapperFolderPath), 'projects.json'), projectRes);
84
-
85
- log.success(`Project created successfully: ${projectRes.uid}`, this.config.context);
86
- log.debug(`Project data saved to: ${this.projectMapperFolderPath}/projects.json`, this.config.context);
105
+ this.updateProgress(true, `project: ${project.name}`, undefined, PROCESS_NAMES.PROJECTS);
106
+ log.success(`Project created successfully: ${projectRes.uid}`, this.config.context);
107
+ } catch (error) {
108
+ this.updateProgress(false, `project: ${project.name}`, (error as any)?.message, PROCESS_NAMES.PROJECTS);
109
+ throw error;
110
+ }
87
111
  }
88
- } else {
89
- this.config.modules.personalize.importData = false; // Stop personalize import if stack not connected to any project
90
- log.warn(`Project file not found: ${projectPath}`, this.config.context);
112
+
113
+ // Only complete progress if we own the progress manager (no parent)
114
+ if (!this.parentProgressManager) {
115
+ this.completeProgress(true);
116
+ }
117
+
118
+ log.success(
119
+ `Projects imported successfully! Total projects: ${projectsCount} - personalization enabled`,
120
+ this.config.context,
121
+ );
122
+ } catch (error) {
123
+ this.config.modules.personalize.importData = false;
124
+ if (!this.parentProgressManager) {
125
+ this.completeProgress(false, (error as any)?.message || 'Project import failed');
126
+ }
127
+ throw error;
91
128
  }
92
129
  }
130
+
131
+ private async analyzeProjects(): Promise<[boolean, number]> {
132
+ return this.withLoadingSpinner('PROJECT: Analyzing import data...', async () => {
133
+ const personalize = this.config.modules.personalize;
134
+ const { dirName, fileName } = personalize.projects;
135
+ const projectPath = join(
136
+ sanitizePath(this.config.data),
137
+ sanitizePath(personalize.dirName),
138
+ sanitizePath(dirName),
139
+ sanitizePath(fileName),
140
+ );
141
+
142
+ log.debug(`Checking for project file: ${projectPath}`, this.config.context);
143
+
144
+ if (!existsSync(projectPath)) {
145
+ this.config.modules.personalize.importData = false;
146
+ log.warn(`Project file not found: ${projectPath}`, this.config.context);
147
+ return [false, 0];
148
+ }
149
+
150
+ this.projectsData = JSON.parse(readFileSync(projectPath, 'utf8')) as CreateProjectInput[];
151
+ const projectsCount = this.projectsData?.length || 0;
152
+
153
+ if (projectsCount < 1) {
154
+ this.config.modules.personalize.importData = false;
155
+ log.warn('No projects found in file', this.config.context);
156
+ return [false, 0];
157
+ }
158
+
159
+ log.debug(`Found ${projectsCount} projects to import`, this.config.context);
160
+ return [true, projectsCount];
161
+ });
162
+ }
93
163
  }