@memberjunction/cli 2.67.0 → 2.69.0

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.
@@ -9,7 +9,16 @@ export default class Pull extends Command {
9
9
  'multi-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
10
  verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
11
  'no-validate': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
+ 'update-existing': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
+ 'create-new': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
14
+ 'backup-before-update': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
15
+ 'merge-strategy': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ 'backup-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ 'preserve-fields': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ 'exclude-fields': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
+ 'target-dir': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
20
  };
13
21
  run(): Promise<void>;
14
22
  private findEntityDirectories;
23
+ private findAllEntityDirectoriesRecursive;
15
24
  }
@@ -1,27 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
26
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
4
  };
@@ -36,6 +13,10 @@ class Pull extends core_1.Command {
36
13
  static examples = [
37
14
  `<%= config.bin %> <%= command.id %> --entity="AI Prompts"`,
38
15
  `<%= config.bin %> <%= command.id %> --entity="AI Prompts" --filter="CategoryID='customer-service-id'"`,
16
+ `<%= config.bin %> <%= command.id %> --entity="AI Agents" --merge-strategy=overwrite`,
17
+ `<%= config.bin %> <%= command.id %> --entity="Actions" --target-dir=./custom-actions --no-validate`,
18
+ `<%= config.bin %> <%= command.id %> --entity="Templates" --dry-run --verbose`,
19
+ `<%= config.bin %> <%= command.id %> --entity="AI Prompts" --exclude-fields=InternalNotes,DebugInfo`,
39
20
  ];
40
21
  static flags = {
41
22
  entity: core_1.Flags.string({ description: 'Entity name to pull', required: true }),
@@ -44,10 +25,23 @@ class Pull extends core_1.Command {
44
25
  'multi-file': core_1.Flags.string({ description: 'Create a single file with multiple records (provide filename)' }),
45
26
  verbose: core_1.Flags.boolean({ char: 'v', description: 'Show detailed output' }),
46
27
  'no-validate': core_1.Flags.boolean({ description: 'Skip validation before pull' }),
28
+ 'update-existing': core_1.Flags.boolean({ description: 'Update existing records during pull', default: true }),
29
+ 'create-new': core_1.Flags.boolean({ description: 'Create new files for records not found locally', default: true }),
30
+ 'backup-before-update': core_1.Flags.boolean({ description: 'Create backups before updating files', default: true }),
31
+ 'merge-strategy': core_1.Flags.string({
32
+ description: 'Merge strategy for updates',
33
+ options: ['merge', 'overwrite', 'skip'],
34
+ default: 'merge'
35
+ }),
36
+ 'backup-directory': core_1.Flags.string({ description: 'Custom backup directory (default: .backups)' }),
37
+ 'preserve-fields': core_1.Flags.string({ description: 'Comma-separated list of fields to preserve during updates', multiple: true }),
38
+ 'exclude-fields': core_1.Flags.string({ description: 'Comma-separated list of fields to exclude from pull', multiple: true }),
39
+ 'target-dir': core_1.Flags.string({ description: 'Specific target directory (overrides auto-discovery)' })
47
40
  };
48
41
  async run() {
49
42
  const { flags } = await this.parse(Pull);
50
43
  const spinner = (0, ora_classic_1.default)();
44
+ let backupManager = null;
51
45
  try {
52
46
  // Load MJ config first
53
47
  spinner.start('Loading configuration');
@@ -68,12 +62,30 @@ class Pull extends core_1.Command {
68
62
  else {
69
63
  spinner.stop();
70
64
  }
71
- // Run validation unless disabled
65
+ // Find entity directories
66
+ const entityDirectories = await this.findEntityDirectories(flags.entity);
67
+ if (entityDirectories.length === 0) {
68
+ this.error(`No directories found for entity "${flags.entity}". Make sure the entity configuration exists in a .mj-sync.json file.`);
69
+ }
70
+ // Select target directory
71
+ let targetDir = flags['target-dir'];
72
+ if (!targetDir) {
73
+ if (entityDirectories.length === 1) {
74
+ targetDir = entityDirectories[0];
75
+ }
76
+ else {
77
+ targetDir = await (0, prompts_1.select)({
78
+ message: `Multiple directories found for entity "${flags.entity}". Which one to use?`,
79
+ choices: entityDirectories.map(dir => ({ name: dir, value: dir }))
80
+ });
81
+ }
82
+ }
83
+ // Run validation on target directory unless disabled
72
84
  if (!flags['no-validate']) {
73
- spinner.start('Validating metadata...');
85
+ spinner.start('Validating target directory...');
74
86
  const validator = new metadata_sync_1.ValidationService({ verbose: flags.verbose });
75
87
  const formatter = new metadata_sync_1.FormattingService();
76
- const validationResult = await validator.validateDirectory(metadata_sync_1.configManager.getOriginalCwd());
88
+ const validationResult = await validator.validateDirectory(targetDir);
77
89
  spinner.stop();
78
90
  if (!validationResult.isValid || validationResult.warnings.length > 0) {
79
91
  // Show validation results
@@ -97,62 +109,94 @@ class Pull extends core_1.Command {
97
109
  this.log(chalk_1.default.green('✓ Validation passed'));
98
110
  }
99
111
  }
100
- // Set target directory if specified via environment
101
- const envTargetDir = process.env.METADATA_SYNC_TARGET_DIR;
112
+ // Initialize backup manager if backups are enabled
113
+ if (flags['backup-before-update']) {
114
+ backupManager = new metadata_sync_1.FileBackupManager();
115
+ await backupManager.initialize();
116
+ }
102
117
  // Create pull service and execute
103
118
  const pullService = new metadata_sync_1.PullService(syncEngine, (0, metadata_sync_1.getSystemUser)());
104
- try {
105
- const result = await pullService.pull({
106
- entity: flags.entity,
107
- filter: flags.filter,
108
- dryRun: flags['dry-run'],
109
- multiFile: flags['multi-file'],
110
- verbose: flags.verbose,
111
- noValidate: flags['no-validate'],
112
- targetDir: envTargetDir
113
- }, {
114
- onProgress: (message) => {
115
- spinner.start(message);
116
- },
117
- onSuccess: (message) => {
118
- spinner.succeed(message);
119
- },
120
- onError: (message) => {
121
- spinner.fail(message);
122
- },
123
- onWarn: (message) => {
124
- this.warn(message);
125
- },
126
- onLog: (message) => {
127
- this.log(message);
128
- }
129
- });
130
- if (!flags['dry-run']) {
131
- this.log(`\n✅ Pull completed successfully`);
119
+ // Build pull options - only include CLI flags that were explicitly provided
120
+ const pullOptions = {
121
+ entity: flags.entity,
122
+ targetDir,
123
+ dryRun: flags['dry-run'],
124
+ verbose: flags.verbose,
125
+ noValidate: flags['no-validate']
126
+ };
127
+ // Add optional flags only if explicitly provided to avoid overriding entity config
128
+ if (flags.filter !== undefined)
129
+ pullOptions.filter = flags.filter;
130
+ if (flags['multi-file'] !== undefined)
131
+ pullOptions.multiFile = flags['multi-file'];
132
+ if (flags['update-existing'] !== undefined)
133
+ pullOptions.updateExistingRecords = flags['update-existing'];
134
+ if (flags['create-new'] !== undefined)
135
+ pullOptions.createNewFileIfNotFound = flags['create-new'];
136
+ if (flags['backup-before-update'] !== undefined)
137
+ pullOptions.backupBeforeUpdate = flags['backup-before-update'];
138
+ if (flags['merge-strategy'] !== undefined)
139
+ pullOptions.mergeStrategy = flags['merge-strategy'];
140
+ if (flags['backup-directory'] !== undefined)
141
+ pullOptions.backupDirectory = flags['backup-directory'];
142
+ if (flags['preserve-fields'] !== undefined)
143
+ pullOptions.preserveFields = flags['preserve-fields'];
144
+ if (flags['exclude-fields'] !== undefined)
145
+ pullOptions.excludeFields = flags['exclude-fields'];
146
+ await pullService.pull(pullOptions, {
147
+ onProgress: (message) => {
148
+ spinner.start(message);
149
+ },
150
+ onSuccess: (message) => {
151
+ spinner.succeed(message);
152
+ },
153
+ onError: (message) => {
154
+ spinner.fail(message);
155
+ },
156
+ onWarn: (message) => {
157
+ this.warn(message);
158
+ },
159
+ onLog: (message) => {
160
+ this.log(message);
161
+ }
162
+ });
163
+ // Clean up backups on success
164
+ if (backupManager && !flags['dry-run']) {
165
+ await backupManager.cleanup();
166
+ if (flags.verbose) {
167
+ this.log(chalk_1.default.green('✓ Temporary backup files cleaned up'));
132
168
  }
133
169
  }
134
- catch (pullError) {
135
- // If it's a "multiple directories found" error, handle it specially
136
- if (pullError instanceof Error && pullError.message.includes('Multiple directories found')) {
137
- // Re-throw to be caught by outer handler
138
- throw pullError;
170
+ // Clean up persistent backup files created by PullService
171
+ if (!flags['dry-run']) {
172
+ try {
173
+ const backupCount = pullService.getCreatedBackupFiles().length;
174
+ await pullService.cleanupBackupFiles();
175
+ if (flags.verbose && backupCount > 0) {
176
+ this.log(chalk_1.default.green(`✓ Cleaned up ${backupCount} persistent backup files`));
177
+ }
178
+ }
179
+ catch (cleanupError) {
180
+ this.warn(`Failed to cleanup persistent backup files: ${cleanupError}`);
139
181
  }
140
- throw pullError;
182
+ }
183
+ if (!flags['dry-run']) {
184
+ this.log(`\n✅ Pull completed successfully`);
141
185
  }
142
186
  }
143
187
  catch (error) {
144
188
  spinner.fail('Pull failed');
145
- // Handle multiple directories error
146
- if (error instanceof Error && error.message.includes('Multiple directories found')) {
147
- const entityDirs = await this.findEntityDirectories(flags.entity);
148
- const targetDir = await (0, prompts_1.select)({
149
- message: `Multiple directories found for entity "${flags.entity}". Which one to use?`,
150
- choices: entityDirs.map(dir => ({ name: dir, value: dir }))
151
- });
152
- // Re-run with specific target directory
153
- process.env.METADATA_SYNC_TARGET_DIR = targetDir;
154
- await this.run();
155
- return;
189
+ // Rollback backups on error
190
+ if (backupManager) {
191
+ try {
192
+ await backupManager.rollback();
193
+ if (flags.verbose) {
194
+ this.log(chalk_1.default.yellow('↩️ Backup files restored'));
195
+ }
196
+ }
197
+ catch (rollbackError) {
198
+ this.warn(`Failed to rollback backup files: ${rollbackError}`);
199
+ }
156
200
  }
157
201
  // Enhanced error logging
158
202
  this.log('\n=== Pull Error Details ===');
@@ -177,9 +221,52 @@ class Pull extends core_1.Command {
177
221
  }
178
222
  }
179
223
  async findEntityDirectories(entityName) {
180
- // This is a simplified version - would need full implementation
181
- const { findEntityDirectories } = await Promise.resolve().then(() => __importStar(require('@memberjunction/metadata-sync')));
182
- return findEntityDirectories(metadata_sync_1.configManager.getOriginalCwd());
224
+ try {
225
+ const workingDir = metadata_sync_1.configManager.getOriginalCwd();
226
+ // Search recursively for all directories with .mj-sync.json files
227
+ const allDirs = this.findAllEntityDirectoriesRecursive(workingDir);
228
+ // Filter directories that match the requested entity
229
+ const entityDirs = [];
230
+ for (const dir of allDirs) {
231
+ try {
232
+ const config = await (0, metadata_sync_1.loadEntityConfig)(dir);
233
+ if (config && config.entity === entityName) {
234
+ entityDirs.push(dir);
235
+ }
236
+ }
237
+ catch (error) {
238
+ this.warn(`Skipping directory ${dir}: invalid configuration (${error})`);
239
+ }
240
+ }
241
+ return entityDirs;
242
+ }
243
+ catch (error) {
244
+ this.error(`Failed to find entity directories: ${error}`);
245
+ }
246
+ }
247
+ findAllEntityDirectoriesRecursive(dir) {
248
+ const fs = require('fs');
249
+ const path = require('path');
250
+ const directories = [];
251
+ try {
252
+ // Check if current directory has .mj-sync.json
253
+ const syncConfigPath = path.join(dir, '.mj-sync.json');
254
+ if (fs.existsSync(syncConfigPath)) {
255
+ directories.push(dir);
256
+ }
257
+ // Recursively search subdirectories
258
+ const items = fs.readdirSync(dir, { withFileTypes: true });
259
+ for (const item of items) {
260
+ if (item.isDirectory() && !item.name.startsWith('.')) {
261
+ const subdirPath = path.join(dir, item.name);
262
+ directories.push(...this.findAllEntityDirectoriesRecursive(subdirPath));
263
+ }
264
+ }
265
+ }
266
+ catch (error) {
267
+ // Skip directories we can't read
268
+ }
269
+ return directories;
183
270
  }
184
271
  }
185
272
  exports.default = Pull;
@@ -304,7 +304,11 @@
304
304
  "description": "Pull metadata from database to local files",
305
305
  "examples": [
306
306
  "<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\"",
307
- "<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\" --filter=\"CategoryID='customer-service-id'\""
307
+ "<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\" --filter=\"CategoryID='customer-service-id'\"",
308
+ "<%= config.bin %> <%= command.id %> --entity=\"AI Agents\" --merge-strategy=overwrite",
309
+ "<%= config.bin %> <%= command.id %> --entity=\"Actions\" --target-dir=./custom-actions --no-validate",
310
+ "<%= config.bin %> <%= command.id %> --entity=\"Templates\" --dry-run --verbose",
311
+ "<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\" --exclude-fields=InternalNotes,DebugInfo"
308
312
  ],
309
313
  "flags": {
310
314
  "entity": {
@@ -347,6 +351,65 @@
347
351
  "name": "no-validate",
348
352
  "allowNo": false,
349
353
  "type": "boolean"
354
+ },
355
+ "update-existing": {
356
+ "description": "Update existing records during pull",
357
+ "name": "update-existing",
358
+ "allowNo": false,
359
+ "type": "boolean"
360
+ },
361
+ "create-new": {
362
+ "description": "Create new files for records not found locally",
363
+ "name": "create-new",
364
+ "allowNo": false,
365
+ "type": "boolean"
366
+ },
367
+ "backup-before-update": {
368
+ "description": "Create backups before updating files",
369
+ "name": "backup-before-update",
370
+ "allowNo": false,
371
+ "type": "boolean"
372
+ },
373
+ "merge-strategy": {
374
+ "description": "Merge strategy for updates",
375
+ "name": "merge-strategy",
376
+ "default": "merge",
377
+ "hasDynamicHelp": false,
378
+ "multiple": false,
379
+ "options": [
380
+ "merge",
381
+ "overwrite",
382
+ "skip"
383
+ ],
384
+ "type": "option"
385
+ },
386
+ "backup-directory": {
387
+ "description": "Custom backup directory (default: .backups)",
388
+ "name": "backup-directory",
389
+ "hasDynamicHelp": false,
390
+ "multiple": false,
391
+ "type": "option"
392
+ },
393
+ "preserve-fields": {
394
+ "description": "Comma-separated list of fields to preserve during updates",
395
+ "name": "preserve-fields",
396
+ "hasDynamicHelp": false,
397
+ "multiple": true,
398
+ "type": "option"
399
+ },
400
+ "exclude-fields": {
401
+ "description": "Comma-separated list of fields to exclude from pull",
402
+ "name": "exclude-fields",
403
+ "hasDynamicHelp": false,
404
+ "multiple": true,
405
+ "type": "option"
406
+ },
407
+ "target-dir": {
408
+ "description": "Specific target directory (overrides auto-discovery)",
409
+ "name": "target-dir",
410
+ "hasDynamicHelp": false,
411
+ "multiple": false,
412
+ "type": "option"
350
413
  }
351
414
  },
352
415
  "hasDynamicHelp": false,
@@ -577,5 +640,5 @@
577
640
  ]
578
641
  }
579
642
  },
580
- "version": "2.67.0"
643
+ "version": "2.69.0"
581
644
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/cli",
3
- "version": "2.67.0",
3
+ "version": "2.69.0",
4
4
  "description": "MemberJunction command line tools",
5
5
  "keywords": [
6
6
  "oclif"
@@ -51,9 +51,9 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@inquirer/prompts": "^5.0.1",
54
- "@memberjunction/codegen-lib": "2.67.0",
55
- "@memberjunction/metadata-sync": "2.67.0",
56
- "@memberjunction/sqlserver-dataprovider": "2.67.0",
54
+ "@memberjunction/codegen-lib": "2.69.0",
55
+ "@memberjunction/metadata-sync": "2.69.0",
56
+ "@memberjunction/sqlserver-dataprovider": "2.69.0",
57
57
  "@oclif/core": "^3",
58
58
  "@oclif/plugin-help": "^6",
59
59
  "@oclif/plugin-version": "^2.0.17",