@memberjunction/metadata-sync 2.55.0 → 2.57.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.
Files changed (67) hide show
  1. package/README.md +92 -51
  2. package/dist/index.d.ts +21 -1
  3. package/dist/index.js +43 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/file-backup-manager.js +2 -2
  6. package/dist/lib/file-backup-manager.js.map +1 -1
  7. package/dist/lib/provider-utils.d.ts +2 -2
  8. package/dist/lib/provider-utils.js.map +1 -1
  9. package/dist/lib/sql-logger.d.ts +44 -0
  10. package/dist/lib/sql-logger.js +140 -0
  11. package/dist/lib/sql-logger.js.map +1 -0
  12. package/dist/lib/sync-engine.d.ts +25 -0
  13. package/dist/lib/sync-engine.js +72 -2
  14. package/dist/lib/sync-engine.js.map +1 -1
  15. package/dist/lib/transaction-manager.d.ts +35 -0
  16. package/dist/lib/transaction-manager.js +100 -0
  17. package/dist/lib/transaction-manager.js.map +1 -0
  18. package/dist/services/FileResetService.d.ts +30 -0
  19. package/dist/services/FileResetService.js +183 -0
  20. package/dist/services/FileResetService.js.map +1 -0
  21. package/dist/services/InitService.d.ts +17 -0
  22. package/dist/services/InitService.js +118 -0
  23. package/dist/services/InitService.js.map +1 -0
  24. package/dist/services/PullService.d.ts +45 -0
  25. package/dist/services/PullService.js +564 -0
  26. package/dist/services/PullService.js.map +1 -0
  27. package/dist/services/PushService.d.ts +47 -0
  28. package/dist/services/PushService.js +558 -0
  29. package/dist/services/PushService.js.map +1 -0
  30. package/dist/services/StatusService.d.ts +32 -0
  31. package/dist/services/StatusService.js +138 -0
  32. package/dist/services/StatusService.js.map +1 -0
  33. package/dist/services/WatchService.d.ts +34 -0
  34. package/dist/services/WatchService.js +296 -0
  35. package/dist/services/WatchService.js.map +1 -0
  36. package/dist/services/index.d.ts +16 -0
  37. package/dist/services/index.js +28 -0
  38. package/dist/services/index.js.map +1 -0
  39. package/package.json +14 -45
  40. package/bin/debug.js +0 -7
  41. package/bin/run +0 -17
  42. package/bin/run.js +0 -6
  43. package/dist/commands/file-reset/index.d.ts +0 -15
  44. package/dist/commands/file-reset/index.js +0 -221
  45. package/dist/commands/file-reset/index.js.map +0 -1
  46. package/dist/commands/init/index.d.ts +0 -7
  47. package/dist/commands/init/index.js +0 -155
  48. package/dist/commands/init/index.js.map +0 -1
  49. package/dist/commands/pull/index.d.ts +0 -246
  50. package/dist/commands/pull/index.js +0 -1448
  51. package/dist/commands/pull/index.js.map +0 -1
  52. package/dist/commands/push/index.d.ts +0 -41
  53. package/dist/commands/push/index.js +0 -1129
  54. package/dist/commands/push/index.js.map +0 -1
  55. package/dist/commands/status/index.d.ts +0 -10
  56. package/dist/commands/status/index.js +0 -199
  57. package/dist/commands/status/index.js.map +0 -1
  58. package/dist/commands/validate/index.d.ts +0 -15
  59. package/dist/commands/validate/index.js +0 -149
  60. package/dist/commands/validate/index.js.map +0 -1
  61. package/dist/commands/watch/index.d.ts +0 -15
  62. package/dist/commands/watch/index.js +0 -300
  63. package/dist/commands/watch/index.js.map +0 -1
  64. package/dist/hooks/init.d.ts +0 -3
  65. package/dist/hooks/init.js +0 -59
  66. package/dist/hooks/init.js.map +0 -1
  67. package/oclif.manifest.json +0 -376
@@ -0,0 +1,32 @@
1
+ import { SyncEngine } from '../lib/sync-engine';
2
+ export interface StatusOptions {
3
+ dir?: string;
4
+ }
5
+ export interface StatusCallbacks {
6
+ onProgress?: (message: string) => void;
7
+ onLog?: (message: string) => void;
8
+ onWarn?: (message: string) => void;
9
+ }
10
+ export interface StatusResult {
11
+ new: number;
12
+ modified: number;
13
+ deleted: number;
14
+ unchanged: number;
15
+ }
16
+ export interface EntityStatusResult {
17
+ entityName: string;
18
+ directory: string;
19
+ new: number;
20
+ modified: number;
21
+ deleted: number;
22
+ unchanged: number;
23
+ }
24
+ export declare class StatusService {
25
+ private syncEngine;
26
+ constructor(syncEngine: SyncEngine);
27
+ checkStatus(options: StatusOptions, callbacks?: StatusCallbacks): Promise<{
28
+ summary: StatusResult;
29
+ details: EntityStatusResult[];
30
+ }>;
31
+ private checkEntityDirectory;
32
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.StatusService = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fast_glob_1 = __importDefault(require("fast-glob"));
10
+ const config_1 = require("../config");
11
+ const provider_utils_1 = require("../lib/provider-utils");
12
+ class StatusService {
13
+ syncEngine;
14
+ constructor(syncEngine) {
15
+ this.syncEngine = syncEngine;
16
+ }
17
+ async checkStatus(options, callbacks) {
18
+ const entityDirs = (0, provider_utils_1.findEntityDirectories)(process.cwd(), options.dir);
19
+ if (entityDirs.length === 0) {
20
+ throw new Error('No entity directories found');
21
+ }
22
+ callbacks?.onLog?.(`Found ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} to check`);
23
+ const details = [];
24
+ let totalNew = 0;
25
+ let totalModified = 0;
26
+ let totalDeleted = 0;
27
+ let totalUnchanged = 0;
28
+ for (const entityDir of entityDirs) {
29
+ const entityConfig = await (0, config_1.loadEntityConfig)(entityDir);
30
+ if (!entityConfig) {
31
+ callbacks?.onWarn?.(`Skipping ${entityDir} - no valid entity configuration`);
32
+ continue;
33
+ }
34
+ callbacks?.onLog?.(`Checking ${entityConfig.entity} in ${entityDir}`);
35
+ const result = await this.checkEntityDirectory(entityDir, entityConfig, callbacks);
36
+ details.push({
37
+ entityName: entityConfig.entity,
38
+ directory: entityDir,
39
+ ...result
40
+ });
41
+ totalNew += result.new;
42
+ totalModified += result.modified;
43
+ totalDeleted += result.deleted;
44
+ totalUnchanged += result.unchanged;
45
+ // Report directory summary
46
+ if (result.new > 0 || result.modified > 0 || result.deleted > 0) {
47
+ callbacks?.onLog?.(` New: ${result.new}, Modified: ${result.modified}, Deleted: ${result.deleted}, Unchanged: ${result.unchanged}`);
48
+ }
49
+ else {
50
+ callbacks?.onLog?.(` All ${result.unchanged} records are up to date`);
51
+ }
52
+ }
53
+ return {
54
+ summary: {
55
+ new: totalNew,
56
+ modified: totalModified,
57
+ deleted: totalDeleted,
58
+ unchanged: totalUnchanged
59
+ },
60
+ details
61
+ };
62
+ }
63
+ async checkEntityDirectory(entityDir, entityConfig, callbacks) {
64
+ const result = { new: 0, modified: 0, deleted: 0, unchanged: 0 };
65
+ // Find files matching the configured pattern
66
+ const pattern = entityConfig.filePattern || '*.json';
67
+ const jsonFiles = await (0, fast_glob_1.default)(pattern, {
68
+ cwd: entityDir,
69
+ ignore: ['.mj-sync.json', '.mj-folder.json', '**/*.backup'],
70
+ dot: true // Include dotfiles (files starting with .)
71
+ });
72
+ for (const file of jsonFiles) {
73
+ try {
74
+ const filePath = path_1.default.join(entityDir, file);
75
+ const recordData = await fs_extra_1.default.readJson(filePath);
76
+ if (recordData.primaryKey) {
77
+ // Check if record exists in database
78
+ const entity = await this.syncEngine.loadEntity(entityConfig.entity, recordData.primaryKey);
79
+ if (!entity) {
80
+ result.deleted++;
81
+ }
82
+ else {
83
+ // Check if modified
84
+ const currentChecksum = this.syncEngine.calculateChecksum(recordData.fields);
85
+ if (recordData.sync?.checksum !== currentChecksum) {
86
+ result.modified++;
87
+ }
88
+ else {
89
+ result.unchanged++;
90
+ }
91
+ }
92
+ }
93
+ else {
94
+ // New record
95
+ result.new++;
96
+ }
97
+ }
98
+ catch (error) {
99
+ callbacks?.onWarn?.(`Failed to check ${file}: ${error}`);
100
+ }
101
+ }
102
+ // Recursively process subdirectories
103
+ const entries = await fs_extra_1.default.readdir(entityDir, { withFileTypes: true });
104
+ for (const entry of entries) {
105
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
106
+ const subDir = path_1.default.join(entityDir, entry.name);
107
+ // Load subdirectory config and merge with parent config
108
+ let subEntityConfig = { ...entityConfig };
109
+ const subDirConfig = await (0, config_1.loadEntityConfig)(subDir);
110
+ if (subDirConfig) {
111
+ // Check if this is a new entity type (has different entity name)
112
+ if (subDirConfig.entity && subDirConfig.entity !== entityConfig.entity) {
113
+ // This is a different entity type, skip it (will be processed separately)
114
+ continue;
115
+ }
116
+ // Merge defaults: parent defaults + subdirectory overrides
117
+ subEntityConfig = {
118
+ ...entityConfig,
119
+ ...subDirConfig,
120
+ defaults: {
121
+ ...entityConfig.defaults,
122
+ ...(subDirConfig.defaults || {})
123
+ }
124
+ };
125
+ }
126
+ // Process subdirectory with merged config
127
+ const subResult = await this.checkEntityDirectory(subDir, subEntityConfig, callbacks);
128
+ result.new += subResult.new;
129
+ result.modified += subResult.modified;
130
+ result.deleted += subResult.deleted;
131
+ result.unchanged += subResult.unchanged;
132
+ }
133
+ }
134
+ return result;
135
+ }
136
+ }
137
+ exports.StatusService = StatusService;
138
+ //# sourceMappingURL=StatusService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StatusService.js","sourceRoot":"","sources":["../../src/services/StatusService.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\nimport fastGlob from 'fast-glob';\nimport { SyncEngine, RecordData } from '../lib/sync-engine';\nimport { loadEntityConfig } from '../config';\nimport { findEntityDirectories } from '../lib/provider-utils';\n\nexport interface StatusOptions {\n dir?: string;\n}\n\nexport interface StatusCallbacks {\n onProgress?: (message: string) => void;\n onLog?: (message: string) => void;\n onWarn?: (message: string) => void;\n}\n\nexport interface StatusResult {\n new: number;\n modified: number;\n deleted: number;\n unchanged: number;\n}\n\nexport interface EntityStatusResult {\n entityName: string;\n directory: string;\n new: number;\n modified: number;\n deleted: number;\n unchanged: number;\n}\n\nexport class StatusService {\n private syncEngine: SyncEngine;\n \n constructor(syncEngine: SyncEngine) {\n this.syncEngine = syncEngine;\n }\n \n async checkStatus(options: StatusOptions, callbacks?: StatusCallbacks): Promise<{\n summary: StatusResult;\n details: EntityStatusResult[];\n }> {\n const entityDirs = findEntityDirectories(process.cwd(), options.dir);\n \n if (entityDirs.length === 0) {\n throw new Error('No entity directories found');\n }\n \n callbacks?.onLog?.(`Found ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} to check`);\n \n const details: EntityStatusResult[] = [];\n let totalNew = 0;\n let totalModified = 0;\n let totalDeleted = 0;\n let totalUnchanged = 0;\n \n for (const entityDir of entityDirs) {\n const entityConfig = await loadEntityConfig(entityDir);\n if (!entityConfig) {\n callbacks?.onWarn?.(`Skipping ${entityDir} - no valid entity configuration`);\n continue;\n }\n \n callbacks?.onLog?.(`Checking ${entityConfig.entity} in ${entityDir}`);\n \n const result = await this.checkEntityDirectory(\n entityDir,\n entityConfig,\n callbacks\n );\n \n details.push({\n entityName: entityConfig.entity,\n directory: entityDir,\n ...result\n });\n \n totalNew += result.new;\n totalModified += result.modified;\n totalDeleted += result.deleted;\n totalUnchanged += result.unchanged;\n \n // Report directory summary\n if (result.new > 0 || result.modified > 0 || result.deleted > 0) {\n callbacks?.onLog?.(` New: ${result.new}, Modified: ${result.modified}, Deleted: ${result.deleted}, Unchanged: ${result.unchanged}`);\n } else {\n callbacks?.onLog?.(` All ${result.unchanged} records are up to date`);\n }\n }\n \n return {\n summary: {\n new: totalNew,\n modified: totalModified,\n deleted: totalDeleted,\n unchanged: totalUnchanged\n },\n details\n };\n }\n \n private async checkEntityDirectory(\n entityDir: string,\n entityConfig: any,\n callbacks?: StatusCallbacks\n ): Promise<StatusResult> {\n const result: StatusResult = { new: 0, modified: 0, deleted: 0, unchanged: 0 };\n \n // Find files matching the configured pattern\n const pattern = entityConfig.filePattern || '*.json';\n const jsonFiles = await fastGlob(pattern, {\n cwd: entityDir,\n ignore: ['.mj-sync.json', '.mj-folder.json', '**/*.backup'],\n dot: true // Include dotfiles (files starting with .)\n });\n \n for (const file of jsonFiles) {\n try {\n const filePath = path.join(entityDir, file);\n const recordData: RecordData = await fs.readJson(filePath);\n \n if (recordData.primaryKey) {\n // Check if record exists in database\n const entity = await this.syncEngine.loadEntity(entityConfig.entity, recordData.primaryKey);\n \n if (!entity) {\n result.deleted++;\n } else {\n // Check if modified\n const currentChecksum = this.syncEngine.calculateChecksum(recordData.fields);\n if (recordData.sync?.checksum !== currentChecksum) {\n result.modified++;\n } else {\n result.unchanged++;\n }\n }\n } else {\n // New record\n result.new++;\n }\n \n } catch (error) {\n callbacks?.onWarn?.(`Failed to check ${file}: ${error}`);\n }\n }\n \n // Recursively process subdirectories\n const entries = await fs.readdir(entityDir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n const subDir = path.join(entityDir, entry.name);\n \n // Load subdirectory config and merge with parent config\n let subEntityConfig = { ...entityConfig };\n const subDirConfig = await loadEntityConfig(subDir);\n \n if (subDirConfig) {\n // Check if this is a new entity type (has different entity name)\n if (subDirConfig.entity && subDirConfig.entity !== entityConfig.entity) {\n // This is a different entity type, skip it (will be processed separately)\n continue;\n }\n \n // Merge defaults: parent defaults + subdirectory overrides\n subEntityConfig = {\n ...entityConfig,\n ...subDirConfig,\n defaults: {\n ...entityConfig.defaults,\n ...(subDirConfig.defaults || {})\n }\n };\n }\n \n // Process subdirectory with merged config\n const subResult = await this.checkEntityDirectory(\n subDir,\n subEntityConfig,\n callbacks\n );\n \n result.new += subResult.new;\n result.modified += subResult.modified;\n result.deleted += subResult.deleted;\n result.unchanged += subResult.unchanged;\n }\n }\n \n return result;\n }\n}"]}
@@ -0,0 +1,34 @@
1
+ import chokidar from 'chokidar';
2
+ import { BaseEntity } from '@memberjunction/core';
3
+ import { SyncEngine } from '../lib/sync-engine';
4
+ export interface WatchOptions {
5
+ dir?: string;
6
+ debounceMs?: number;
7
+ ignorePatterns?: string[];
8
+ }
9
+ export interface WatchCallbacks {
10
+ onFileAdd?: (filePath: string, entityDir: string, entityConfig: any) => void;
11
+ onFileChange?: (filePath: string, entityDir: string, entityConfig: any) => void;
12
+ onFileDelete?: (filePath: string, entityDir: string, entityConfig: any) => void;
13
+ onLog?: (message: string) => void;
14
+ onWarn?: (message: string) => void;
15
+ onError?: (error: Error) => void;
16
+ onRecordCreated?: (entity: BaseEntity, entityConfig: any) => void;
17
+ onRecordUpdated?: (entity: BaseEntity, changes: any, entityConfig: any) => void;
18
+ onRecordSaved?: (entity: BaseEntity, isNew: boolean, entityConfig: any) => void;
19
+ }
20
+ export interface WatchResult {
21
+ watchers: chokidar.FSWatcher[];
22
+ stop: () => Promise<void>;
23
+ }
24
+ export declare class WatchService {
25
+ private syncEngine;
26
+ private debounceTimers;
27
+ private sqlLoggingSession;
28
+ constructor(syncEngine: SyncEngine);
29
+ watch(options?: WatchOptions, callbacks?: WatchCallbacks): Promise<WatchResult>;
30
+ private handleFileChange;
31
+ private syncJsonFile;
32
+ private syncExternalFile;
33
+ private setupSqlLogging;
34
+ }
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WatchService = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chokidar_1 = __importDefault(require("chokidar"));
10
+ const core_1 = require("@memberjunction/core");
11
+ const config_1 = require("../config");
12
+ const provider_utils_1 = require("../lib/provider-utils");
13
+ const config_manager_1 = require("../lib/config-manager");
14
+ class WatchService {
15
+ syncEngine;
16
+ debounceTimers = new Map();
17
+ sqlLoggingSession = null;
18
+ constructor(syncEngine) {
19
+ this.syncEngine = syncEngine;
20
+ }
21
+ async watch(options = {}, callbacks) {
22
+ const entityDirs = (0, provider_utils_1.findEntityDirectories)(process.cwd(), options.dir);
23
+ if (entityDirs.length === 0) {
24
+ throw new Error('No entity directories found');
25
+ }
26
+ callbacks?.onLog?.(`Watching ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} for changes`);
27
+ // Setup SQL logging session
28
+ await this.setupSqlLogging(callbacks);
29
+ // Set up watchers
30
+ const watchers = [];
31
+ for (const entityDir of entityDirs) {
32
+ const entityConfig = await (0, config_1.loadEntityConfig)(entityDir);
33
+ if (!entityConfig) {
34
+ callbacks?.onWarn?.(`Skipping ${entityDir} - no valid entity configuration`);
35
+ continue;
36
+ }
37
+ callbacks?.onLog?.(`Watching ${entityConfig.entity} in ${entityDir}`);
38
+ // Watch for JSON files and external files
39
+ const patterns = [
40
+ path_1.default.join(entityDir, entityConfig.filePattern || '**/*.json'),
41
+ path_1.default.join(entityDir, '**/*.md'),
42
+ path_1.default.join(entityDir, '**/*.txt'),
43
+ path_1.default.join(entityDir, '**/*.html'),
44
+ path_1.default.join(entityDir, '**/*.liquid'),
45
+ path_1.default.join(entityDir, '**/*.sql')
46
+ ];
47
+ const ignored = [
48
+ '**/node_modules/**',
49
+ '**/.git/**',
50
+ '**/.mj-sync.json',
51
+ '**/.mj-folder.json',
52
+ '**/*.backup',
53
+ ...(options.ignorePatterns || [])
54
+ ];
55
+ const watcher = chokidar_1.default.watch(patterns, {
56
+ ignored,
57
+ persistent: true,
58
+ ignoreInitial: true
59
+ });
60
+ watcher
61
+ .on('add', (filePath) => {
62
+ callbacks?.onFileAdd?.(filePath, entityDir, entityConfig);
63
+ this.handleFileChange(filePath, 'added', entityDir, entityConfig, options, callbacks);
64
+ })
65
+ .on('change', (filePath) => {
66
+ callbacks?.onFileChange?.(filePath, entityDir, entityConfig);
67
+ this.handleFileChange(filePath, 'changed', entityDir, entityConfig, options, callbacks);
68
+ })
69
+ .on('unlink', (filePath) => {
70
+ callbacks?.onFileDelete?.(filePath, entityDir, entityConfig);
71
+ this.handleFileChange(filePath, 'deleted', entityDir, entityConfig, options, callbacks);
72
+ });
73
+ watchers.push(watcher);
74
+ }
75
+ return {
76
+ watchers,
77
+ stop: async () => {
78
+ // Clear all debounce timers
79
+ for (const timer of this.debounceTimers.values()) {
80
+ clearTimeout(timer);
81
+ }
82
+ this.debounceTimers.clear();
83
+ // Close all watchers
84
+ await Promise.all(watchers.map(w => w.close()));
85
+ // Dispose SQL logging session
86
+ if (this.sqlLoggingSession) {
87
+ try {
88
+ callbacks?.onLog?.(`📝 SQL log written to: ${this.sqlLoggingSession.filePath}`);
89
+ await this.sqlLoggingSession.dispose();
90
+ this.sqlLoggingSession = null;
91
+ }
92
+ catch (error) {
93
+ callbacks?.onWarn?.(`Failed to close SQL logging session: ${error}`);
94
+ }
95
+ }
96
+ }
97
+ };
98
+ }
99
+ handleFileChange(filePath, event, entityDir, entityConfig, options, callbacks) {
100
+ // Clear existing debounce timer
101
+ const existingTimer = this.debounceTimers.get(filePath);
102
+ if (existingTimer) {
103
+ clearTimeout(existingTimer);
104
+ }
105
+ // Set new debounce timer
106
+ const debounceMs = options.debounceMs || 1000;
107
+ const timer = setTimeout(async () => {
108
+ this.debounceTimers.delete(filePath);
109
+ try {
110
+ const relativePath = path_1.default.relative(entityDir, filePath);
111
+ callbacks?.onLog?.(`File ${event}: ${relativePath}`);
112
+ if (event === 'deleted') {
113
+ // Handle deletion
114
+ callbacks?.onLog?.('File deletion detected - manual database cleanup may be required');
115
+ }
116
+ else if (filePath.endsWith('.json')) {
117
+ // Handle JSON file change
118
+ await this.syncJsonFile(filePath, entityDir, entityConfig, callbacks);
119
+ }
120
+ else {
121
+ // Handle external file change
122
+ await this.syncExternalFile(filePath, entityDir, entityConfig, callbacks);
123
+ }
124
+ }
125
+ catch (error) {
126
+ const errorMessage = `Failed to sync ${filePath}: ${error.message || error}`;
127
+ callbacks?.onWarn?.(errorMessage);
128
+ if (error instanceof Error) {
129
+ callbacks?.onError?.(error);
130
+ }
131
+ }
132
+ }, debounceMs);
133
+ this.debounceTimers.set(filePath, timer);
134
+ }
135
+ async syncJsonFile(filePath, entityDir, entityConfig, callbacks) {
136
+ const recordData = await fs_extra_1.default.readJson(filePath);
137
+ // Keep original fields for file writing
138
+ const originalFields = { ...recordData.fields };
139
+ // Build defaults
140
+ const defaults = await this.syncEngine.buildDefaults(filePath, entityConfig);
141
+ // Load or create entity
142
+ let entity = null;
143
+ let isNew = false;
144
+ if (recordData.primaryKey) {
145
+ entity = await this.syncEngine.loadEntity(entityConfig.entity, recordData.primaryKey);
146
+ }
147
+ if (!entity) {
148
+ // New record
149
+ entity = await this.syncEngine.createEntityObject(entityConfig.entity);
150
+ entity.NewRecord();
151
+ isNew = true;
152
+ }
153
+ // Apply defaults first
154
+ for (const [field, value] of Object.entries(defaults)) {
155
+ if (field in entity) {
156
+ entity[field] = value;
157
+ }
158
+ }
159
+ // Apply record fields with processed values for database operations
160
+ for (const [field, value] of Object.entries(recordData.fields)) {
161
+ if (field in entity) {
162
+ const processedValue = await this.syncEngine.processFieldValue(value, path_1.default.dirname(filePath));
163
+ entity[field] = processedValue;
164
+ }
165
+ }
166
+ // Check if the record is dirty before saving
167
+ let wasActuallyUpdated = false;
168
+ let changes = null;
169
+ if (!isNew && entity.Dirty) {
170
+ // Record is dirty, get the changes
171
+ changes = entity.GetChangesSinceLastSave();
172
+ const changeKeys = Object.keys(changes);
173
+ if (changeKeys.length > 0) {
174
+ wasActuallyUpdated = true;
175
+ // Get primary key info for display
176
+ const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);
177
+ const primaryKeyDisplay = [];
178
+ if (entityInfo) {
179
+ for (const pk of entityInfo.PrimaryKeys) {
180
+ primaryKeyDisplay.push(`${pk.Name}: ${entity.Get(pk.Name)}`);
181
+ }
182
+ }
183
+ callbacks?.onLog?.(`📝 Updating ${entityConfig.entity} record:`);
184
+ if (primaryKeyDisplay.length > 0) {
185
+ callbacks?.onLog?.(` Primary Key: ${primaryKeyDisplay.join(', ')}`);
186
+ }
187
+ callbacks?.onLog?.(` Changes:`);
188
+ for (const fieldName of changeKeys) {
189
+ const field = entity.GetFieldByName(fieldName);
190
+ const oldValue = field ? field.OldValue : undefined;
191
+ const newValue = changes[fieldName];
192
+ callbacks?.onLog?.(` ${fieldName}: ${oldValue} → ${newValue}`);
193
+ }
194
+ callbacks?.onRecordUpdated?.(entity, changes, entityConfig);
195
+ }
196
+ }
197
+ else if (isNew) {
198
+ wasActuallyUpdated = true;
199
+ callbacks?.onRecordCreated?.(entity, entityConfig);
200
+ }
201
+ // Save the record
202
+ const saved = await entity.Save();
203
+ if (!saved) {
204
+ const message = entity.LatestResult?.Message;
205
+ if (message) {
206
+ throw new Error(`Failed to save record: ${message}`);
207
+ }
208
+ const errors = entity.LatestResult?.Errors?.map(err => typeof err === 'string' ? err : (err?.message || JSON.stringify(err)))?.join(', ') || 'Unknown error';
209
+ throw new Error(`Failed to save record: ${errors}`);
210
+ }
211
+ if (wasActuallyUpdated) {
212
+ callbacks?.onLog?.(`Successfully ${isNew ? 'created' : 'updated'} ${entityConfig.entity} record`);
213
+ callbacks?.onRecordSaved?.(entity, isNew, entityConfig);
214
+ }
215
+ else {
216
+ callbacks?.onLog?.(`No changes detected for ${entityConfig.entity} record - skipped update`);
217
+ }
218
+ // Update the local file with primary key and sync metadata
219
+ if (wasActuallyUpdated) {
220
+ const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);
221
+ if (entityInfo) {
222
+ // Update primary key for new records
223
+ if (isNew) {
224
+ const newPrimaryKey = {};
225
+ for (const pk of entityInfo.PrimaryKeys) {
226
+ newPrimaryKey[pk.Name] = entity.Get(pk.Name);
227
+ }
228
+ recordData.primaryKey = newPrimaryKey;
229
+ }
230
+ // Always update sync metadata when the record was updated - use original fields for checksum
231
+ recordData.sync = {
232
+ lastModified: new Date().toISOString(),
233
+ checksum: await this.syncEngine.calculateChecksumWithFileContent(originalFields, path_1.default.dirname(filePath))
234
+ };
235
+ // Restore original field values to preserve @ references
236
+ recordData.fields = originalFields;
237
+ // Write back to file
238
+ await fs_extra_1.default.writeJson(filePath, recordData, { spaces: 2 });
239
+ }
240
+ }
241
+ }
242
+ async syncExternalFile(filePath, entityDir, entityConfig, callbacks) {
243
+ // Find the corresponding JSON file
244
+ const fileName = path_1.default.basename(filePath);
245
+ const parts = fileName.split('.');
246
+ if (parts.length >= 3) {
247
+ // Format: uuid.fieldname.ext
248
+ const jsonFileName = `${parts[0]}.json`;
249
+ const fieldName = parts[1];
250
+ const jsonFilePath = path_1.default.join(path_1.default.dirname(filePath), jsonFileName);
251
+ if (await fs_extra_1.default.pathExists(jsonFilePath)) {
252
+ // Update the JSON file's sync metadata to trigger a sync
253
+ const recordData = await fs_extra_1.default.readJson(jsonFilePath);
254
+ recordData.sync = {
255
+ lastModified: new Date().toISOString(),
256
+ checksum: recordData.sync?.checksum || ''
257
+ };
258
+ await fs_extra_1.default.writeJson(jsonFilePath, recordData, { spaces: 2 });
259
+ callbacks?.onLog?.(`Updated sync metadata for ${jsonFileName} due to external file change`);
260
+ }
261
+ }
262
+ }
263
+ async setupSqlLogging(callbacks) {
264
+ try {
265
+ // Load sync config for SQL logging settings
266
+ const syncConfig = await (0, config_1.loadSyncConfig)(config_manager_1.configManager.getOriginalCwd());
267
+ if (syncConfig?.sqlLogging?.enabled) {
268
+ const provider = core_1.Metadata.Provider; // SQLServerDataProvider
269
+ if (provider && typeof provider.CreateSqlLogger === 'function') {
270
+ // Generate filename with timestamp
271
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
272
+ const filename = syncConfig.sqlLogging?.formatAsMigration
273
+ ? `MetadataSync_Watch_${timestamp}.sql`
274
+ : `watch_${timestamp}.sql`;
275
+ // Use .sql-log-watch directory in the working directory
276
+ const outputDir = path_1.default.join(config_manager_1.configManager.getOriginalCwd(), '.sql-log-watch');
277
+ const filepath = path_1.default.join(outputDir, filename);
278
+ // Ensure the directory exists
279
+ await fs_extra_1.default.ensureDir(outputDir);
280
+ // Create the SQL logging session
281
+ this.sqlLoggingSession = await provider.CreateSqlLogger(filepath, {
282
+ formatAsMigration: syncConfig.sqlLogging?.formatAsMigration || false,
283
+ description: 'MetadataSync watch operation',
284
+ logRecordChangeMetadata: true
285
+ });
286
+ callbacks?.onLog?.(`📝 SQL logging enabled: ${filepath}`);
287
+ }
288
+ }
289
+ }
290
+ catch (error) {
291
+ callbacks?.onWarn?.(`Failed to setup SQL logging: ${error}`);
292
+ }
293
+ }
294
+ }
295
+ exports.WatchService = WatchService;
296
+ //# sourceMappingURL=WatchService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WatchService.js","sourceRoot":"","sources":["../../src/services/WatchService.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\nimport chokidar from 'chokidar';\nimport { BaseEntity, Metadata } from '@memberjunction/core';\nimport { SyncEngine, RecordData } from '../lib/sync-engine';\nimport { loadEntityConfig, loadSyncConfig } from '../config';\nimport { findEntityDirectories } from '../lib/provider-utils';\nimport { configManager } from '../lib/config-manager';\nimport type { SqlLoggingSession } from '@memberjunction/sqlserver-dataprovider';\n\nexport interface WatchOptions {\n dir?: string;\n debounceMs?: number;\n ignorePatterns?: string[];\n}\n\nexport interface WatchCallbacks {\n onFileAdd?: (filePath: string, entityDir: string, entityConfig: any) => void;\n onFileChange?: (filePath: string, entityDir: string, entityConfig: any) => void;\n onFileDelete?: (filePath: string, entityDir: string, entityConfig: any) => void;\n onLog?: (message: string) => void;\n onWarn?: (message: string) => void;\n onError?: (error: Error) => void;\n onRecordCreated?: (entity: BaseEntity, entityConfig: any) => void;\n onRecordUpdated?: (entity: BaseEntity, changes: any, entityConfig: any) => void;\n onRecordSaved?: (entity: BaseEntity, isNew: boolean, entityConfig: any) => void;\n}\n\nexport interface WatchResult {\n watchers: chokidar.FSWatcher[];\n stop: () => Promise<void>;\n}\n\nexport class WatchService {\n private syncEngine: SyncEngine;\n private debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n private sqlLoggingSession: SqlLoggingSession | null = null;\n \n constructor(syncEngine: SyncEngine) {\n this.syncEngine = syncEngine;\n }\n \n async watch(options: WatchOptions = {}, callbacks?: WatchCallbacks): Promise<WatchResult> {\n const entityDirs = findEntityDirectories(process.cwd(), options.dir);\n \n if (entityDirs.length === 0) {\n throw new Error('No entity directories found');\n }\n \n callbacks?.onLog?.(`Watching ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} for changes`);\n \n // Setup SQL logging session\n await this.setupSqlLogging(callbacks);\n \n // Set up watchers\n const watchers: chokidar.FSWatcher[] = [];\n \n for (const entityDir of entityDirs) {\n const entityConfig = await loadEntityConfig(entityDir);\n if (!entityConfig) {\n callbacks?.onWarn?.(`Skipping ${entityDir} - no valid entity configuration`);\n continue;\n }\n \n callbacks?.onLog?.(`Watching ${entityConfig.entity} in ${entityDir}`);\n \n // Watch for JSON files and external files\n const patterns = [\n path.join(entityDir, entityConfig.filePattern || '**/*.json'),\n path.join(entityDir, '**/*.md'),\n path.join(entityDir, '**/*.txt'),\n path.join(entityDir, '**/*.html'),\n path.join(entityDir, '**/*.liquid'),\n path.join(entityDir, '**/*.sql')\n ];\n \n const ignored = [\n '**/node_modules/**',\n '**/.git/**',\n '**/.mj-sync.json',\n '**/.mj-folder.json',\n '**/*.backup',\n ...(options.ignorePatterns || [])\n ];\n \n const watcher = chokidar.watch(patterns, {\n ignored,\n persistent: true,\n ignoreInitial: true\n });\n \n watcher\n .on('add', (filePath) => {\n callbacks?.onFileAdd?.(filePath, entityDir, entityConfig);\n this.handleFileChange(filePath, 'added', entityDir, entityConfig, options, callbacks);\n })\n .on('change', (filePath) => {\n callbacks?.onFileChange?.(filePath, entityDir, entityConfig);\n this.handleFileChange(filePath, 'changed', entityDir, entityConfig, options, callbacks);\n })\n .on('unlink', (filePath) => {\n callbacks?.onFileDelete?.(filePath, entityDir, entityConfig);\n this.handleFileChange(filePath, 'deleted', entityDir, entityConfig, options, callbacks);\n });\n \n watchers.push(watcher);\n }\n \n return {\n watchers,\n stop: async () => {\n // Clear all debounce timers\n for (const timer of this.debounceTimers.values()) {\n clearTimeout(timer);\n }\n this.debounceTimers.clear();\n \n // Close all watchers\n await Promise.all(watchers.map(w => w.close()));\n \n // Dispose SQL logging session\n if (this.sqlLoggingSession) {\n try {\n callbacks?.onLog?.(`📝 SQL log written to: ${this.sqlLoggingSession.filePath}`);\n await this.sqlLoggingSession.dispose();\n this.sqlLoggingSession = null;\n } catch (error) {\n callbacks?.onWarn?.(`Failed to close SQL logging session: ${error}`);\n }\n }\n }\n };\n }\n \n private handleFileChange(\n filePath: string,\n event: string,\n entityDir: string,\n entityConfig: any,\n options: WatchOptions,\n callbacks?: WatchCallbacks\n ): void {\n // Clear existing debounce timer\n const existingTimer = this.debounceTimers.get(filePath);\n if (existingTimer) {\n clearTimeout(existingTimer);\n }\n \n // Set new debounce timer\n const debounceMs = options.debounceMs || 1000;\n const timer = setTimeout(async () => {\n this.debounceTimers.delete(filePath);\n \n try {\n const relativePath = path.relative(entityDir, filePath);\n callbacks?.onLog?.(`File ${event}: ${relativePath}`);\n \n if (event === 'deleted') {\n // Handle deletion\n callbacks?.onLog?.('File deletion detected - manual database cleanup may be required');\n } else if (filePath.endsWith('.json')) {\n // Handle JSON file change\n await this.syncJsonFile(filePath, entityDir, entityConfig, callbacks);\n } else {\n // Handle external file change\n await this.syncExternalFile(filePath, entityDir, entityConfig, callbacks);\n }\n } catch (error) {\n const errorMessage = `Failed to sync ${filePath}: ${(error as any).message || error}`;\n callbacks?.onWarn?.(errorMessage);\n if (error instanceof Error) {\n callbacks?.onError?.(error);\n }\n }\n }, debounceMs);\n \n this.debounceTimers.set(filePath, timer);\n }\n \n private async syncJsonFile(\n filePath: string,\n entityDir: string,\n entityConfig: any,\n callbacks?: WatchCallbacks\n ): Promise<void> {\n const recordData: RecordData = await fs.readJson(filePath);\n \n // Keep original fields for file writing\n const originalFields = { ...recordData.fields };\n \n // Build defaults\n const defaults = await this.syncEngine.buildDefaults(filePath, entityConfig);\n \n // Load or create entity\n let entity: BaseEntity | null = null;\n let isNew = false;\n \n if (recordData.primaryKey) {\n entity = await this.syncEngine.loadEntity(entityConfig.entity, recordData.primaryKey);\n }\n \n if (!entity) {\n // New record\n entity = await this.syncEngine.createEntityObject(entityConfig.entity);\n entity.NewRecord();\n isNew = true;\n }\n \n // Apply defaults first\n for (const [field, value] of Object.entries(defaults)) {\n if (field in entity) {\n (entity as any)[field] = value;\n }\n }\n \n // Apply record fields with processed values for database operations\n for (const [field, value] of Object.entries(recordData.fields)) {\n if (field in entity) {\n const processedValue = await this.syncEngine.processFieldValue(value, path.dirname(filePath));\n (entity as any)[field] = processedValue;\n }\n }\n \n // Check if the record is dirty before saving\n let wasActuallyUpdated = false;\n let changes: any = null;\n \n if (!isNew && entity.Dirty) {\n // Record is dirty, get the changes\n changes = entity.GetChangesSinceLastSave();\n const changeKeys = Object.keys(changes);\n if (changeKeys.length > 0) {\n wasActuallyUpdated = true;\n \n // Get primary key info for display\n const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);\n const primaryKeyDisplay: string[] = [];\n if (entityInfo) {\n for (const pk of entityInfo.PrimaryKeys) {\n primaryKeyDisplay.push(`${pk.Name}: ${entity.Get(pk.Name)}`);\n }\n }\n \n callbacks?.onLog?.(`📝 Updating ${entityConfig.entity} record:`);\n if (primaryKeyDisplay.length > 0) {\n callbacks?.onLog?.(` Primary Key: ${primaryKeyDisplay.join(', ')}`);\n }\n callbacks?.onLog?.(` Changes:`);\n for (const fieldName of changeKeys) {\n const field = entity.GetFieldByName(fieldName);\n const oldValue = field ? field.OldValue : undefined;\n const newValue = (changes as any)[fieldName];\n callbacks?.onLog?.(` ${fieldName}: ${oldValue} → ${newValue}`);\n }\n \n callbacks?.onRecordUpdated?.(entity, changes, entityConfig);\n }\n } else if (isNew) {\n wasActuallyUpdated = true;\n callbacks?.onRecordCreated?.(entity, entityConfig);\n }\n \n // Save the record\n const saved = await entity.Save();\n if (!saved) {\n const message = entity.LatestResult?.Message;\n if (message) {\n throw new Error(`Failed to save record: ${message}`);\n }\n \n const errors = entity.LatestResult?.Errors?.map(err => \n typeof err === 'string' ? err : (err?.message || JSON.stringify(err))\n )?.join(', ') || 'Unknown error';\n throw new Error(`Failed to save record: ${errors}`);\n }\n \n if (wasActuallyUpdated) {\n callbacks?.onLog?.(`Successfully ${isNew ? 'created' : 'updated'} ${entityConfig.entity} record`);\n callbacks?.onRecordSaved?.(entity, isNew, entityConfig);\n } else {\n callbacks?.onLog?.(`No changes detected for ${entityConfig.entity} record - skipped update`);\n }\n \n // Update the local file with primary key and sync metadata\n if (wasActuallyUpdated) {\n const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);\n if (entityInfo) {\n // Update primary key for new records\n if (isNew) {\n const newPrimaryKey: Record<string, any> = {};\n for (const pk of entityInfo.PrimaryKeys) {\n newPrimaryKey[pk.Name] = entity.Get(pk.Name);\n }\n recordData.primaryKey = newPrimaryKey;\n }\n \n // Always update sync metadata when the record was updated - use original fields for checksum\n recordData.sync = {\n lastModified: new Date().toISOString(),\n checksum: await this.syncEngine.calculateChecksumWithFileContent(originalFields, path.dirname(filePath))\n };\n \n // Restore original field values to preserve @ references\n recordData.fields = originalFields;\n \n // Write back to file\n await fs.writeJson(filePath, recordData, { spaces: 2 });\n }\n }\n }\n \n private async syncExternalFile(\n filePath: string,\n entityDir: string,\n entityConfig: any,\n callbacks?: WatchCallbacks\n ): Promise<void> {\n // Find the corresponding JSON file\n const fileName = path.basename(filePath);\n const parts = fileName.split('.');\n \n if (parts.length >= 3) {\n // Format: uuid.fieldname.ext\n const jsonFileName = `${parts[0]}.json`;\n const fieldName = parts[1];\n const jsonFilePath = path.join(path.dirname(filePath), jsonFileName);\n \n if (await fs.pathExists(jsonFilePath)) {\n // Update the JSON file's sync metadata to trigger a sync\n const recordData: RecordData = await fs.readJson(jsonFilePath);\n recordData.sync = {\n lastModified: new Date().toISOString(),\n checksum: recordData.sync?.checksum || ''\n };\n await fs.writeJson(jsonFilePath, recordData, { spaces: 2 });\n \n callbacks?.onLog?.(`Updated sync metadata for ${jsonFileName} due to external file change`);\n }\n }\n }\n \n private async setupSqlLogging(callbacks?: WatchCallbacks): Promise<void> {\n try {\n // Load sync config for SQL logging settings\n const syncConfig = await loadSyncConfig(configManager.getOriginalCwd());\n \n if (syncConfig?.sqlLogging?.enabled) {\n const provider = Metadata.Provider as any; // SQLServerDataProvider\n \n if (provider && typeof provider.CreateSqlLogger === 'function') {\n // Generate filename with timestamp\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const filename = syncConfig.sqlLogging?.formatAsMigration \n ? `MetadataSync_Watch_${timestamp}.sql`\n : `watch_${timestamp}.sql`;\n \n // Use .sql-log-watch directory in the working directory\n const outputDir = path.join(configManager.getOriginalCwd(), '.sql-log-watch');\n const filepath = path.join(outputDir, filename);\n \n // Ensure the directory exists\n await fs.ensureDir(outputDir);\n \n // Create the SQL logging session\n this.sqlLoggingSession = await provider.CreateSqlLogger(filepath, {\n formatAsMigration: syncConfig.sqlLogging?.formatAsMigration || false,\n description: 'MetadataSync watch operation',\n logRecordChangeMetadata: true\n });\n \n callbacks?.onLog?.(`📝 SQL logging enabled: ${filepath}`);\n }\n }\n } catch (error) {\n callbacks?.onWarn?.(`Failed to setup SQL logging: ${error}`);\n }\n }\n}"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @fileoverview Service exports for MetadataSync library usage
3
+ * @module services
4
+ *
5
+ * This module exports all the service classes that can be used programmatically
6
+ * without the CLI interface. These services provide the core functionality
7
+ * for metadata synchronization operations.
8
+ */
9
+ export { InitService, InitOptions, InitCallbacks } from './InitService';
10
+ export { PullService, PullOptions, PullCallbacks, PullResult } from './PullService';
11
+ export { PushService, PushOptions, PushCallbacks, PushResult, EntityPushResult } from './PushService';
12
+ export { ValidationService } from './ValidationService';
13
+ export { FormattingService } from './FormattingService';
14
+ export { StatusService, StatusOptions, StatusCallbacks, StatusResult, EntityStatusResult } from './StatusService';
15
+ export { FileResetService, FileResetOptions, FileResetCallbacks, FileResetResult } from './FileResetService';
16
+ export { WatchService, WatchOptions, WatchCallbacks, WatchResult } from './WatchService';
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Service exports for MetadataSync library usage
4
+ * @module services
5
+ *
6
+ * This module exports all the service classes that can be used programmatically
7
+ * without the CLI interface. These services provide the core functionality
8
+ * for metadata synchronization operations.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.WatchService = exports.FileResetService = exports.StatusService = exports.FormattingService = exports.ValidationService = exports.PushService = exports.PullService = exports.InitService = void 0;
12
+ var InitService_1 = require("./InitService");
13
+ Object.defineProperty(exports, "InitService", { enumerable: true, get: function () { return InitService_1.InitService; } });
14
+ var PullService_1 = require("./PullService");
15
+ Object.defineProperty(exports, "PullService", { enumerable: true, get: function () { return PullService_1.PullService; } });
16
+ var PushService_1 = require("./PushService");
17
+ Object.defineProperty(exports, "PushService", { enumerable: true, get: function () { return PushService_1.PushService; } });
18
+ var ValidationService_1 = require("./ValidationService");
19
+ Object.defineProperty(exports, "ValidationService", { enumerable: true, get: function () { return ValidationService_1.ValidationService; } });
20
+ var FormattingService_1 = require("./FormattingService");
21
+ Object.defineProperty(exports, "FormattingService", { enumerable: true, get: function () { return FormattingService_1.FormattingService; } });
22
+ var StatusService_1 = require("./StatusService");
23
+ Object.defineProperty(exports, "StatusService", { enumerable: true, get: function () { return StatusService_1.StatusService; } });
24
+ var FileResetService_1 = require("./FileResetService");
25
+ Object.defineProperty(exports, "FileResetService", { enumerable: true, get: function () { return FileResetService_1.FileResetService; } });
26
+ var WatchService_1 = require("./WatchService");
27
+ Object.defineProperty(exports, "WatchService", { enumerable: true, get: function () { return WatchService_1.WatchService; } });
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,6CAAwE;AAA/D,0GAAA,WAAW,OAAA;AACpB,6CAAoF;AAA3E,0GAAA,WAAW,OAAA;AACpB,6CAAsG;AAA7F,0GAAA,WAAW,OAAA;AACpB,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,iDAAkH;AAAzG,8GAAA,aAAa,OAAA;AACtB,uDAA6G;AAApG,oHAAA,gBAAgB,OAAA;AACzB,+CAAyF;AAAhF,4GAAA,YAAY,OAAA","sourcesContent":["/**\n * @fileoverview Service exports for MetadataSync library usage\n * @module services\n * \n * This module exports all the service classes that can be used programmatically\n * without the CLI interface. These services provide the core functionality\n * for metadata synchronization operations.\n */\n\nexport { InitService, InitOptions, InitCallbacks } from './InitService';\nexport { PullService, PullOptions, PullCallbacks, PullResult } from './PullService';\nexport { PushService, PushOptions, PushCallbacks, PushResult, EntityPushResult } from './PushService';\nexport { ValidationService } from './ValidationService';\nexport { FormattingService } from './FormattingService';\nexport { StatusService, StatusOptions, StatusCallbacks, StatusResult, EntityStatusResult } from './StatusService';\nexport { FileResetService, FileResetOptions, FileResetCallbacks, FileResetResult } from './FileResetService';\nexport { WatchService, WatchOptions, WatchCallbacks, WatchResult } from './WatchService';"]}