@magentrix-corp/magentrix-cli 1.3.16 → 1.3.17

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 (68) hide show
  1. package/LICENSE +25 -25
  2. package/README.md +1166 -1166
  3. package/actions/autopublish.old.js +293 -293
  4. package/actions/config.js +182 -182
  5. package/actions/create.js +466 -466
  6. package/actions/help.js +164 -164
  7. package/actions/iris/buildStage.js +874 -874
  8. package/actions/iris/delete.js +256 -256
  9. package/actions/iris/dev.js +391 -391
  10. package/actions/iris/index.js +6 -6
  11. package/actions/iris/link.js +375 -375
  12. package/actions/iris/recover.js +268 -268
  13. package/actions/main.js +80 -80
  14. package/actions/publish.js +1420 -1420
  15. package/actions/pull.js +684 -684
  16. package/actions/setup.js +148 -148
  17. package/actions/status.js +17 -17
  18. package/actions/update.js +248 -248
  19. package/bin/magentrix.js +393 -393
  20. package/package.json +55 -55
  21. package/utils/assetPaths.js +158 -158
  22. package/utils/autopublishLock.js +77 -77
  23. package/utils/cacher.js +206 -206
  24. package/utils/cli/checkInstanceUrl.js +76 -74
  25. package/utils/cli/helpers/compare.js +282 -282
  26. package/utils/cli/helpers/ensureApiKey.js +63 -63
  27. package/utils/cli/helpers/ensureCredentials.js +68 -68
  28. package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
  29. package/utils/cli/writeRecords.js +262 -262
  30. package/utils/compare.js +135 -135
  31. package/utils/compress.js +17 -17
  32. package/utils/config.js +527 -527
  33. package/utils/debug.js +144 -144
  34. package/utils/diagnostics/testPublishLogic.js +96 -96
  35. package/utils/diff.js +49 -49
  36. package/utils/downloadAssets.js +291 -291
  37. package/utils/filetag.js +115 -115
  38. package/utils/hash.js +14 -14
  39. package/utils/iris/backup.js +411 -411
  40. package/utils/iris/builder.js +541 -541
  41. package/utils/iris/config-reader.js +664 -664
  42. package/utils/iris/deleteHelper.js +150 -150
  43. package/utils/iris/errors.js +537 -537
  44. package/utils/iris/linker.js +601 -601
  45. package/utils/iris/lock.js +360 -360
  46. package/utils/iris/validation.js +360 -360
  47. package/utils/iris/validator.js +281 -281
  48. package/utils/iris/zipper.js +248 -248
  49. package/utils/logger.js +291 -291
  50. package/utils/magentrix/api/assets.js +220 -220
  51. package/utils/magentrix/api/auth.js +107 -107
  52. package/utils/magentrix/api/createEntity.js +61 -61
  53. package/utils/magentrix/api/deleteEntity.js +55 -55
  54. package/utils/magentrix/api/iris.js +251 -251
  55. package/utils/magentrix/api/meqlQuery.js +36 -36
  56. package/utils/magentrix/api/retrieveEntity.js +86 -86
  57. package/utils/magentrix/api/updateEntity.js +66 -66
  58. package/utils/magentrix/fetch.js +168 -168
  59. package/utils/merge.js +22 -22
  60. package/utils/permissionError.js +70 -70
  61. package/utils/preferences.js +40 -40
  62. package/utils/progress.js +469 -469
  63. package/utils/spinner.js +43 -43
  64. package/utils/template.js +52 -52
  65. package/utils/updateFileBase.js +121 -121
  66. package/utils/workspaces.js +108 -108
  67. package/vars/config.js +11 -11
  68. package/vars/global.js +50 -50
package/utils/config.js CHANGED
@@ -1,527 +1,527 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
-
5
- /**
6
- * Config class handles global and per-project configuration.
7
- * - Global config: ~/.config/magentrix/config.json (or %APPDATA%\magentrix\config.json on Windows)
8
- * - Project config: ./.magentrix/config.json (walks up dirs to support monorepos)
9
- * - Secure file/folder permissions (Unix)
10
- * - Optionally, global config can be namespaced by pathHash (SHA256 hash of a directory path)
11
- * - Supports searching through config data like a database
12
- */
13
- class Config {
14
- /**
15
- * @param {Object} opts
16
- * @param {string} opts.projectDir - Directory to treat as project root (default: process.cwd())
17
- * @param {string} opts.projectFolder - Name of per-project config folder (default: '.magentrix')
18
- * @param {string} opts.configFile - Config file name (default: 'config.json')
19
- * @param {string} opts.globalAppName - Name for global config dir (default: 'magentrix')
20
- */
21
- constructor({
22
- projectDir = process.cwd(),
23
- projectFolder = '.magentrix',
24
- configFile = 'config.json',
25
- globalAppName = 'magentrix',
26
- } = {}) {
27
- this.projectDir = projectDir;
28
- this.projectFolder = projectFolder;
29
- this.configFile = configFile;
30
- this.globalAppName = globalAppName;
31
-
32
- // File paths for global and project config
33
- this.globalConfigPath = this.getGlobalConfigPath();
34
- this.projectConfigPath = this.getProjectConfigPath();
35
-
36
- // In-memory cache for config files
37
- this._globalConfig = null;
38
- this._projectConfig = null;
39
- }
40
-
41
- /**
42
- * Determine the global config file path:
43
- * - Linux/macOS: ~/.config/[app]/config.json
44
- * - Windows: %APPDATA%/[app]/config.json
45
- */
46
- getGlobalConfigPath() {
47
- const base =
48
- process.platform === 'win32'
49
- ? path.join(process.env.APPDATA, this.globalAppName)
50
- : path.join(os.homedir(), '.config', this.globalAppName);
51
-
52
- if (!fs.existsSync(base)) fs.mkdirSync(base, { recursive: true, mode: 0o700 });
53
- return path.join(base, this.configFile);
54
- }
55
-
56
- /**
57
- * Determine the project config file path:
58
- * - Walks up from current directory looking for .magentrix/config.json.
59
- * - If not found, uses current working directory as default.
60
- */
61
- getProjectConfigPath() {
62
- let dir = this.projectDir;
63
- while (dir !== path.dirname(dir)) {
64
- const cfgPath = path.join(dir, this.projectFolder, this.configFile);
65
- if (fs.existsSync(cfgPath)) return cfgPath;
66
- dir = path.dirname(dir);
67
- }
68
-
69
- // Fallback: create project config folder in current directory
70
- const fallbackDir = path.join(this.projectDir, this.projectFolder);
71
- if (!fs.existsSync(fallbackDir)) fs.mkdirSync(fallbackDir, { recursive: true, mode: 0o700 });
72
- return path.join(fallbackDir, this.configFile);
73
- }
74
-
75
- /**
76
- * Internal: Load config JSON from file for project or global config.
77
- * @param {'project'|'global'} type
78
- * @param {string} [customPath] - Optional custom file path for project loads
79
- * @returns {Object} Parsed config, or empty object if missing.
80
- */
81
- _loadConfig(type = 'project', customPath = null) {
82
- const cfgPath = type === 'global'
83
- ? this.globalConfigPath
84
- : (customPath || this.projectConfigPath);
85
-
86
- try {
87
- if (fs.existsSync(cfgPath)) {
88
- const data = fs.readFileSync(cfgPath, 'utf8');
89
- return JSON.parse(data);
90
- }
91
- } catch (e) {
92
- throw new Error(`Failed to read ${type} config at ${cfgPath}: ${e.message}`);
93
- }
94
- return {};
95
- }
96
-
97
- /**
98
- * Internal: Write config JSON to file, using secure file permissions.
99
- * @param {Object} obj - The config object to save.
100
- * @param {'project'|'global'} type
101
- * @param {string} [customPath] - Optional custom file path for project saves
102
- */
103
- _saveConfig(obj, type = 'project', customPath = null) {
104
- const cfgPath = type === 'global'
105
- ? this.globalConfigPath
106
- : (customPath || this.projectConfigPath);
107
-
108
- const data = JSON.stringify(obj, null, 2);
109
- try {
110
- fs.writeFileSync(cfgPath, data, { mode: 0o600 });
111
- } catch (e) {
112
- throw new Error(`Failed to write ${type} config at ${cfgPath}: ${e.message}`);
113
- }
114
- }
115
-
116
- /**
117
- * Read a value from config (global or project).
118
- * If opts.pathHash is specified in global mode, data is stored under that subkey.
119
- * For project reads, opts.filename can specify a custom filename.
120
- * @param {string} [key] - The config key to read. If missing, returns entire config object.
121
- * @param {Object} opts
122
- * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
123
- * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
124
- * @param {string} [opts.filename] - Optional. Custom filename for project reads (ignored for global).
125
- * @returns {*} The value for the key, or full config object.
126
- */
127
- read(key, opts = {}) {
128
- const isGlobal = opts.global === true;
129
- const filename = opts.filename;
130
- const pathHash = opts.pathHash;
131
-
132
- if (isGlobal) {
133
- if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
134
- if (pathHash) {
135
- this._globalConfig[pathHash] = this._globalConfig[pathHash] || {};
136
- return key
137
- ? this._globalConfig[pathHash][key]
138
- : { ...this._globalConfig[pathHash] };
139
- }
140
- return key
141
- ? this._globalConfig[key]
142
- : { ...this._globalConfig };
143
- } else {
144
- if (filename) {
145
- const projectFolderPath = path.join(this.projectDir, this.projectFolder);
146
- const customPath = path.join(projectFolderPath, filename);
147
- const customConfig = this._loadConfig('project', customPath);
148
- return key
149
- ? customConfig[key]
150
- : { ...customConfig };
151
- } else {
152
- if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
153
- return key
154
- ? this._projectConfig[key]
155
- : { ...this._projectConfig };
156
- }
157
- }
158
- }
159
-
160
- /**
161
- * Remove a key from config (global or project).
162
- * If opts.pathHash is specified in global mode, key is removed from that subkey.
163
- * For project removals, opts.filename can specify a custom filename.
164
- *
165
- * @param {string} key - The config key to remove.
166
- * @param {Object} opts
167
- * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
168
- * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
169
- * @param {string} [opts.filename] - Optional. Custom filename for project config.
170
- */
171
- removeKey(key, opts = {}) {
172
- const isGlobal = opts.global === true;
173
- const filename = opts.filename;
174
- const pathHash = opts.pathHash;
175
-
176
- if (isGlobal) {
177
- if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
178
-
179
- if (pathHash) {
180
- if (!this._globalConfig[pathHash]) return;
181
- delete this._globalConfig[pathHash][key];
182
- } else {
183
- delete this._globalConfig[key];
184
- }
185
-
186
- this._saveConfig(this._globalConfig, 'global');
187
- } else {
188
- if (filename) {
189
- const projectFolderPath = path.join(this.projectDir, this.projectFolder);
190
- if (!fs.existsSync(projectFolderPath)) return;
191
- const customPath = path.join(projectFolderPath, filename);
192
- const customConfig = this._loadConfig('project', customPath) || {};
193
- delete customConfig[key];
194
- this._saveConfig(customConfig, 'project', customPath);
195
- } else {
196
- if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
197
- delete this._projectConfig[key];
198
- this._saveConfig(this._projectConfig, 'project');
199
- }
200
- }
201
- }
202
-
203
- /**
204
- * Remove multiple keys from config (global or project) in a single save operation.
205
- *
206
- * @param {string[]} keys - Array of keys to remove.
207
- * @param {Object} opts
208
- * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
209
- * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
210
- * @param {string} [opts.filename] - Optional. Custom filename for project config.
211
- */
212
- removeKeys(keys, opts = {}) {
213
- const isGlobal = opts.global === true;
214
- const filename = opts.filename;
215
- const pathHash = opts.pathHash;
216
-
217
- if (!Array.isArray(keys) || keys.length === 0) return;
218
-
219
- if (isGlobal) {
220
- if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
221
-
222
- let changed = false;
223
- if (pathHash) {
224
- if (!this._globalConfig[pathHash]) return;
225
- for (const key of keys) {
226
- if (key in this._globalConfig[pathHash]) {
227
- delete this._globalConfig[pathHash][key];
228
- changed = true;
229
- }
230
- }
231
- } else {
232
- for (const key of keys) {
233
- if (key in this._globalConfig) {
234
- delete this._globalConfig[key];
235
- changed = true;
236
- }
237
- }
238
- }
239
-
240
- if (changed) {
241
- this._saveConfig(this._globalConfig, 'global');
242
- }
243
- } else {
244
- if (filename) {
245
- const projectFolderPath = path.join(this.projectDir, this.projectFolder);
246
- if (!fs.existsSync(projectFolderPath)) return;
247
- const customPath = path.join(projectFolderPath, filename);
248
- const customConfig = this._loadConfig('project', customPath) || {};
249
-
250
- let changed = false;
251
- for (const key of keys) {
252
- if (key in customConfig) {
253
- delete customConfig[key];
254
- changed = true;
255
- }
256
- }
257
-
258
- if (changed) {
259
- this._saveConfig(customConfig, 'project', customPath);
260
- }
261
- } else {
262
- if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
263
-
264
- let changed = false;
265
- for (const key of keys) {
266
- if (key in this._projectConfig) {
267
- delete this._projectConfig[key];
268
- changed = true;
269
- }
270
- }
271
-
272
- if (changed) {
273
- this._saveConfig(this._projectConfig, 'project');
274
- }
275
- }
276
- }
277
- }
278
-
279
-
280
- /**
281
- * Save a key-value pair to config (global or project).
282
- * If opts.pathHash is specified in global mode, data is stored under that subkey.
283
- * For project saves, opts.filename can specify a custom filename.
284
- * @param {string} key - The config key.
285
- * @param {*} value - The value to store.
286
- * @param {Object} opts
287
- * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
288
- * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
289
- * @param {string} [opts.filename] - Optional. Custom filename for project saves (ignored for global).
290
- */
291
- save(key, value, opts = {}) {
292
- const isGlobal = opts.global === true;
293
- const filename = opts.filename;
294
- const pathHash = opts.pathHash;
295
-
296
- if (isGlobal) {
297
- if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
298
- if (pathHash) {
299
- this._globalConfig[pathHash] = this._globalConfig[pathHash] || {};
300
- this._globalConfig[pathHash][key] = value;
301
- } else {
302
- this._globalConfig[key] = value;
303
- }
304
- this._saveConfig(this._globalConfig, 'global');
305
- } else {
306
- if (filename) {
307
- // Ensure project config folder exists
308
- const projectFolderPath = path.join(this.projectDir, this.projectFolder);
309
- if (!fs.existsSync(projectFolderPath)) {
310
- fs.mkdirSync(projectFolderPath, { recursive: true, mode: 0o700 });
311
- }
312
- const customPath = path.join(projectFolderPath, filename);
313
- // Load, update, and save the custom config file independently
314
- const customConfig = this._loadConfig('project', customPath) || {};
315
- customConfig[key] = value;
316
- this._saveConfig(customConfig, 'project', customPath);
317
- } else {
318
- // Default project config
319
- if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
320
- this._projectConfig[key] = value;
321
- this._saveConfig(this._projectConfig, 'project');
322
- }
323
- }
324
- }
325
-
326
- /**
327
- * Internal: Deep search through nested objects and arrays
328
- * @param {*} obj - The object/value to search through
329
- * @param {Object} query - The query object with key-value pairs to match
330
- * @param {Object} [options={}] - Search options
331
- * @param {boolean} [options.exact=true] - Whether to use exact matching or partial matching
332
- * @param {boolean} [options.caseSensitive=true] - Whether string comparisons should be case sensitive
333
- * @returns {boolean} True if the object matches the query
334
- */
335
- _matchesQuery(obj, query, options = {}) {
336
- const { exact = true, caseSensitive = true } = options;
337
- if (obj === null || obj === undefined) return false;
338
- for (const [queryKey, expectedValue] of Object.entries(query)) {
339
- if (!this._hasMatchingValue(obj, queryKey, expectedValue, options)) {
340
- return false;
341
- }
342
- }
343
- return true;
344
- }
345
-
346
- /**
347
- * Internal: Check if an object has a matching value for a given key (supports nested keys)
348
- * @param {*} obj - The object to search in
349
- * @param {string} key - The key to search for (supports dot notation for nested keys)
350
- * @param {*} expectedValue - The expected value
351
- * @param {Object} options - Search options
352
- * @returns {boolean} True if a matching value is found
353
- */
354
- _hasMatchingValue(obj, key, expectedValue, options) {
355
- const { exact, caseSensitive } = options;
356
- const keys = key.split('.');
357
- let current = obj;
358
-
359
- for (const k of keys) {
360
- if (current === null || current === undefined) return false;
361
-
362
- if (Array.isArray(current)) {
363
- return current.some(item =>
364
- this._hasMatchingValue(item, keys.slice(keys.indexOf(k)).join('.'), expectedValue, options)
365
- );
366
- } else if (typeof current === 'object') {
367
- current = current[k];
368
- } else {
369
- return false;
370
- }
371
- }
372
-
373
- return this._compareValues(current, expectedValue, options);
374
- }
375
-
376
- /**
377
- * Internal: Compare two values based on search options
378
- * @param {*} actual - The actual value from the object
379
- * @param {*} expected - The expected value from the query
380
- * @param {Object} options - Comparison options
381
- * @returns {boolean} True if values match according to options
382
- */
383
- _compareValues(actual, expected, options) {
384
- const { exact, caseSensitive } = options;
385
- if (exact) {
386
- if (
387
- typeof actual === 'string' &&
388
- typeof expected === 'string' &&
389
- !caseSensitive
390
- ) {
391
- return actual.toLowerCase() === expected.toLowerCase();
392
- }
393
- return actual === expected;
394
- }
395
-
396
- // Partial matching for strings
397
- if (
398
- typeof actual === 'string' &&
399
- typeof expected === 'string'
400
- ) {
401
- const a = caseSensitive ? actual : actual.toLowerCase();
402
- const e = caseSensitive ? expected : expected.toLowerCase();
403
- return a.includes(e);
404
- }
405
-
406
- return actual === expected;
407
- }
408
-
409
- /**
410
- * Search through config data like a database query.
411
- * Supports nested object searching and various matching options.
412
- *
413
- * @param {Object} query - Query object with key-value pairs to match
414
- * @param {Object} [opts={}] - Search options
415
- * @param {boolean} [opts.global=false] - Search in global config if true, project config if false
416
- * @param {string} [opts.filename] - Custom filename for project searches
417
- * @param {string} [opts.pathHash] - Hash for namespaced global config searches
418
- * @param {boolean} [opts.exact=true] - Use exact matching (false for partial string matching)
419
- * @param {boolean} [opts.caseSensitive=true] - Case sensitive string comparisons
420
- * @param {string} [opts.searchIn] - Specific top-level key to search within (optional)
421
- *
422
- * @returns {Array} Array of matching objects/values with their keys
423
- */
424
- searchObject(query, opts = {}) {
425
- const {
426
- global = false,
427
- filename,
428
- pathHash,
429
- exact = true,
430
- caseSensitive = true,
431
- searchIn,
432
- } = opts;
433
-
434
- let configData = global
435
- ? this.read(null, { global: true, pathHash })
436
- : this.read(null, { global: false, filename });
437
-
438
- if (searchIn && configData[searchIn]) {
439
- configData = { [searchIn]: configData[searchIn] };
440
- }
441
-
442
- const results = [];
443
- const recurse = (obj, keyPath = '') => {
444
- if (obj === null || obj === undefined) return;
445
- if (typeof obj === 'object' && this._matchesQuery(obj, query, { exact, caseSensitive })) {
446
- results.push({ key: keyPath, value: obj, matches: query });
447
- }
448
- if (typeof obj === 'object') {
449
- if (Array.isArray(obj)) {
450
- obj.forEach((item, idx) => recurse(item, `${keyPath}[${idx}]`));
451
- } else {
452
- Object.entries(obj).forEach(([k, v]) =>
453
- recurse(v, keyPath ? `${keyPath}.${k}` : k)
454
- );
455
- }
456
- }
457
- };
458
- recurse(configData);
459
- return results;
460
- }
461
-
462
- /**
463
- * Convenience method for simple key-value searches
464
- * @param {string} key - The key to search for
465
- * @param {*} value - The value to match
466
- * @param {Object} [opts={}] - Same options as searchObject
467
- * @returns {Array} Array of matching results
468
- */
469
- findByKeyValue(key, value, opts = {}) {
470
- return this.searchObject({ [key]: value }, opts);
471
- }
472
-
473
- /**
474
- * Search for objects containing any of the specified values
475
- * @param {Object} query - Query object with key-value pairs (OR logic)
476
- * @param {Object} [opts={}] - Same options as searchObject
477
- * @returns {Array} Array of matching results
478
- */
479
- searchObjectAny(query, opts = {}) {
480
- const {
481
- global = false,
482
- filename,
483
- pathHash,
484
- exact = true,
485
- caseSensitive = true,
486
- searchIn,
487
- } = opts;
488
-
489
- let configData = global
490
- ? this.read(null, { global: true, pathHash })
491
- : this.read(null, { global: false, filename });
492
-
493
- if (searchIn && configData[searchIn]) {
494
- configData = { [searchIn]: configData[searchIn] };
495
- }
496
-
497
- const results = [];
498
- const recurseAny = (obj, keyPath = '') => {
499
- if (obj === null || obj === undefined) return;
500
- if (typeof obj === 'object') {
501
- const matched = Object.entries(query).filter(([qk, qv]) =>
502
- this._hasMatchingValue(obj, qk, qv, { exact, caseSensitive })
503
- );
504
- if (matched.length) {
505
- results.push({
506
- key: keyPath,
507
- value: obj,
508
- matches: Object.fromEntries(matched),
509
- });
510
- }
511
- }
512
- if (typeof obj === 'object') {
513
- if (Array.isArray(obj)) {
514
- obj.forEach((item, idx) => recurseAny(item, `${keyPath}[${idx}]`));
515
- } else {
516
- Object.entries(obj).forEach(([k, v]) =>
517
- recurseAny(v, keyPath ? `${keyPath}.${k}` : k)
518
- );
519
- }
520
- }
521
- };
522
- recurseAny(configData);
523
- return results;
524
- }
525
- }
526
-
527
- export default Config;
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ /**
6
+ * Config class handles global and per-project configuration.
7
+ * - Global config: ~/.config/magentrix/config.json (or %APPDATA%\magentrix\config.json on Windows)
8
+ * - Project config: ./.magentrix/config.json (walks up dirs to support monorepos)
9
+ * - Secure file/folder permissions (Unix)
10
+ * - Optionally, global config can be namespaced by pathHash (SHA256 hash of a directory path)
11
+ * - Supports searching through config data like a database
12
+ */
13
+ class Config {
14
+ /**
15
+ * @param {Object} opts
16
+ * @param {string} opts.projectDir - Directory to treat as project root (default: process.cwd())
17
+ * @param {string} opts.projectFolder - Name of per-project config folder (default: '.magentrix')
18
+ * @param {string} opts.configFile - Config file name (default: 'config.json')
19
+ * @param {string} opts.globalAppName - Name for global config dir (default: 'magentrix')
20
+ */
21
+ constructor({
22
+ projectDir = process.cwd(),
23
+ projectFolder = '.magentrix',
24
+ configFile = 'config.json',
25
+ globalAppName = 'magentrix',
26
+ } = {}) {
27
+ this.projectDir = projectDir;
28
+ this.projectFolder = projectFolder;
29
+ this.configFile = configFile;
30
+ this.globalAppName = globalAppName;
31
+
32
+ // File paths for global and project config
33
+ this.globalConfigPath = this.getGlobalConfigPath();
34
+ this.projectConfigPath = this.getProjectConfigPath();
35
+
36
+ // In-memory cache for config files
37
+ this._globalConfig = null;
38
+ this._projectConfig = null;
39
+ }
40
+
41
+ /**
42
+ * Determine the global config file path:
43
+ * - Linux/macOS: ~/.config/[app]/config.json
44
+ * - Windows: %APPDATA%/[app]/config.json
45
+ */
46
+ getGlobalConfigPath() {
47
+ const base =
48
+ process.platform === 'win32'
49
+ ? path.join(process.env.APPDATA, this.globalAppName)
50
+ : path.join(os.homedir(), '.config', this.globalAppName);
51
+
52
+ if (!fs.existsSync(base)) fs.mkdirSync(base, { recursive: true, mode: 0o700 });
53
+ return path.join(base, this.configFile);
54
+ }
55
+
56
+ /**
57
+ * Determine the project config file path:
58
+ * - Walks up from current directory looking for .magentrix/config.json.
59
+ * - If not found, uses current working directory as default.
60
+ */
61
+ getProjectConfigPath() {
62
+ let dir = this.projectDir;
63
+ while (dir !== path.dirname(dir)) {
64
+ const cfgPath = path.join(dir, this.projectFolder, this.configFile);
65
+ if (fs.existsSync(cfgPath)) return cfgPath;
66
+ dir = path.dirname(dir);
67
+ }
68
+
69
+ // Fallback: create project config folder in current directory
70
+ const fallbackDir = path.join(this.projectDir, this.projectFolder);
71
+ if (!fs.existsSync(fallbackDir)) fs.mkdirSync(fallbackDir, { recursive: true, mode: 0o700 });
72
+ return path.join(fallbackDir, this.configFile);
73
+ }
74
+
75
+ /**
76
+ * Internal: Load config JSON from file for project or global config.
77
+ * @param {'project'|'global'} type
78
+ * @param {string} [customPath] - Optional custom file path for project loads
79
+ * @returns {Object} Parsed config, or empty object if missing.
80
+ */
81
+ _loadConfig(type = 'project', customPath = null) {
82
+ const cfgPath = type === 'global'
83
+ ? this.globalConfigPath
84
+ : (customPath || this.projectConfigPath);
85
+
86
+ try {
87
+ if (fs.existsSync(cfgPath)) {
88
+ const data = fs.readFileSync(cfgPath, 'utf8');
89
+ return JSON.parse(data);
90
+ }
91
+ } catch (e) {
92
+ throw new Error(`Failed to read ${type} config at ${cfgPath}: ${e.message}`);
93
+ }
94
+ return {};
95
+ }
96
+
97
+ /**
98
+ * Internal: Write config JSON to file, using secure file permissions.
99
+ * @param {Object} obj - The config object to save.
100
+ * @param {'project'|'global'} type
101
+ * @param {string} [customPath] - Optional custom file path for project saves
102
+ */
103
+ _saveConfig(obj, type = 'project', customPath = null) {
104
+ const cfgPath = type === 'global'
105
+ ? this.globalConfigPath
106
+ : (customPath || this.projectConfigPath);
107
+
108
+ const data = JSON.stringify(obj, null, 2);
109
+ try {
110
+ fs.writeFileSync(cfgPath, data, { mode: 0o600 });
111
+ } catch (e) {
112
+ throw new Error(`Failed to write ${type} config at ${cfgPath}: ${e.message}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Read a value from config (global or project).
118
+ * If opts.pathHash is specified in global mode, data is stored under that subkey.
119
+ * For project reads, opts.filename can specify a custom filename.
120
+ * @param {string} [key] - The config key to read. If missing, returns entire config object.
121
+ * @param {Object} opts
122
+ * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
123
+ * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
124
+ * @param {string} [opts.filename] - Optional. Custom filename for project reads (ignored for global).
125
+ * @returns {*} The value for the key, or full config object.
126
+ */
127
+ read(key, opts = {}) {
128
+ const isGlobal = opts.global === true;
129
+ const filename = opts.filename;
130
+ const pathHash = opts.pathHash;
131
+
132
+ if (isGlobal) {
133
+ if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
134
+ if (pathHash) {
135
+ this._globalConfig[pathHash] = this._globalConfig[pathHash] || {};
136
+ return key
137
+ ? this._globalConfig[pathHash][key]
138
+ : { ...this._globalConfig[pathHash] };
139
+ }
140
+ return key
141
+ ? this._globalConfig[key]
142
+ : { ...this._globalConfig };
143
+ } else {
144
+ if (filename) {
145
+ const projectFolderPath = path.join(this.projectDir, this.projectFolder);
146
+ const customPath = path.join(projectFolderPath, filename);
147
+ const customConfig = this._loadConfig('project', customPath);
148
+ return key
149
+ ? customConfig[key]
150
+ : { ...customConfig };
151
+ } else {
152
+ if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
153
+ return key
154
+ ? this._projectConfig[key]
155
+ : { ...this._projectConfig };
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Remove a key from config (global or project).
162
+ * If opts.pathHash is specified in global mode, key is removed from that subkey.
163
+ * For project removals, opts.filename can specify a custom filename.
164
+ *
165
+ * @param {string} key - The config key to remove.
166
+ * @param {Object} opts
167
+ * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
168
+ * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
169
+ * @param {string} [opts.filename] - Optional. Custom filename for project config.
170
+ */
171
+ removeKey(key, opts = {}) {
172
+ const isGlobal = opts.global === true;
173
+ const filename = opts.filename;
174
+ const pathHash = opts.pathHash;
175
+
176
+ if (isGlobal) {
177
+ if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
178
+
179
+ if (pathHash) {
180
+ if (!this._globalConfig[pathHash]) return;
181
+ delete this._globalConfig[pathHash][key];
182
+ } else {
183
+ delete this._globalConfig[key];
184
+ }
185
+
186
+ this._saveConfig(this._globalConfig, 'global');
187
+ } else {
188
+ if (filename) {
189
+ const projectFolderPath = path.join(this.projectDir, this.projectFolder);
190
+ if (!fs.existsSync(projectFolderPath)) return;
191
+ const customPath = path.join(projectFolderPath, filename);
192
+ const customConfig = this._loadConfig('project', customPath) || {};
193
+ delete customConfig[key];
194
+ this._saveConfig(customConfig, 'project', customPath);
195
+ } else {
196
+ if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
197
+ delete this._projectConfig[key];
198
+ this._saveConfig(this._projectConfig, 'project');
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Remove multiple keys from config (global or project) in a single save operation.
205
+ *
206
+ * @param {string[]} keys - Array of keys to remove.
207
+ * @param {Object} opts
208
+ * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
209
+ * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
210
+ * @param {string} [opts.filename] - Optional. Custom filename for project config.
211
+ */
212
+ removeKeys(keys, opts = {}) {
213
+ const isGlobal = opts.global === true;
214
+ const filename = opts.filename;
215
+ const pathHash = opts.pathHash;
216
+
217
+ if (!Array.isArray(keys) || keys.length === 0) return;
218
+
219
+ if (isGlobal) {
220
+ if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
221
+
222
+ let changed = false;
223
+ if (pathHash) {
224
+ if (!this._globalConfig[pathHash]) return;
225
+ for (const key of keys) {
226
+ if (key in this._globalConfig[pathHash]) {
227
+ delete this._globalConfig[pathHash][key];
228
+ changed = true;
229
+ }
230
+ }
231
+ } else {
232
+ for (const key of keys) {
233
+ if (key in this._globalConfig) {
234
+ delete this._globalConfig[key];
235
+ changed = true;
236
+ }
237
+ }
238
+ }
239
+
240
+ if (changed) {
241
+ this._saveConfig(this._globalConfig, 'global');
242
+ }
243
+ } else {
244
+ if (filename) {
245
+ const projectFolderPath = path.join(this.projectDir, this.projectFolder);
246
+ if (!fs.existsSync(projectFolderPath)) return;
247
+ const customPath = path.join(projectFolderPath, filename);
248
+ const customConfig = this._loadConfig('project', customPath) || {};
249
+
250
+ let changed = false;
251
+ for (const key of keys) {
252
+ if (key in customConfig) {
253
+ delete customConfig[key];
254
+ changed = true;
255
+ }
256
+ }
257
+
258
+ if (changed) {
259
+ this._saveConfig(customConfig, 'project', customPath);
260
+ }
261
+ } else {
262
+ if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
263
+
264
+ let changed = false;
265
+ for (const key of keys) {
266
+ if (key in this._projectConfig) {
267
+ delete this._projectConfig[key];
268
+ changed = true;
269
+ }
270
+ }
271
+
272
+ if (changed) {
273
+ this._saveConfig(this._projectConfig, 'project');
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+
280
+ /**
281
+ * Save a key-value pair to config (global or project).
282
+ * If opts.pathHash is specified in global mode, data is stored under that subkey.
283
+ * For project saves, opts.filename can specify a custom filename.
284
+ * @param {string} key - The config key.
285
+ * @param {*} value - The value to store.
286
+ * @param {Object} opts
287
+ * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
288
+ * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
289
+ * @param {string} [opts.filename] - Optional. Custom filename for project saves (ignored for global).
290
+ */
291
+ save(key, value, opts = {}) {
292
+ const isGlobal = opts.global === true;
293
+ const filename = opts.filename;
294
+ const pathHash = opts.pathHash;
295
+
296
+ if (isGlobal) {
297
+ if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
298
+ if (pathHash) {
299
+ this._globalConfig[pathHash] = this._globalConfig[pathHash] || {};
300
+ this._globalConfig[pathHash][key] = value;
301
+ } else {
302
+ this._globalConfig[key] = value;
303
+ }
304
+ this._saveConfig(this._globalConfig, 'global');
305
+ } else {
306
+ if (filename) {
307
+ // Ensure project config folder exists
308
+ const projectFolderPath = path.join(this.projectDir, this.projectFolder);
309
+ if (!fs.existsSync(projectFolderPath)) {
310
+ fs.mkdirSync(projectFolderPath, { recursive: true, mode: 0o700 });
311
+ }
312
+ const customPath = path.join(projectFolderPath, filename);
313
+ // Load, update, and save the custom config file independently
314
+ const customConfig = this._loadConfig('project', customPath) || {};
315
+ customConfig[key] = value;
316
+ this._saveConfig(customConfig, 'project', customPath);
317
+ } else {
318
+ // Default project config
319
+ if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
320
+ this._projectConfig[key] = value;
321
+ this._saveConfig(this._projectConfig, 'project');
322
+ }
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Internal: Deep search through nested objects and arrays
328
+ * @param {*} obj - The object/value to search through
329
+ * @param {Object} query - The query object with key-value pairs to match
330
+ * @param {Object} [options={}] - Search options
331
+ * @param {boolean} [options.exact=true] - Whether to use exact matching or partial matching
332
+ * @param {boolean} [options.caseSensitive=true] - Whether string comparisons should be case sensitive
333
+ * @returns {boolean} True if the object matches the query
334
+ */
335
+ _matchesQuery(obj, query, options = {}) {
336
+ const { exact = true, caseSensitive = true } = options;
337
+ if (obj === null || obj === undefined) return false;
338
+ for (const [queryKey, expectedValue] of Object.entries(query)) {
339
+ if (!this._hasMatchingValue(obj, queryKey, expectedValue, options)) {
340
+ return false;
341
+ }
342
+ }
343
+ return true;
344
+ }
345
+
346
+ /**
347
+ * Internal: Check if an object has a matching value for a given key (supports nested keys)
348
+ * @param {*} obj - The object to search in
349
+ * @param {string} key - The key to search for (supports dot notation for nested keys)
350
+ * @param {*} expectedValue - The expected value
351
+ * @param {Object} options - Search options
352
+ * @returns {boolean} True if a matching value is found
353
+ */
354
+ _hasMatchingValue(obj, key, expectedValue, options) {
355
+ const { exact, caseSensitive } = options;
356
+ const keys = key.split('.');
357
+ let current = obj;
358
+
359
+ for (const k of keys) {
360
+ if (current === null || current === undefined) return false;
361
+
362
+ if (Array.isArray(current)) {
363
+ return current.some(item =>
364
+ this._hasMatchingValue(item, keys.slice(keys.indexOf(k)).join('.'), expectedValue, options)
365
+ );
366
+ } else if (typeof current === 'object') {
367
+ current = current[k];
368
+ } else {
369
+ return false;
370
+ }
371
+ }
372
+
373
+ return this._compareValues(current, expectedValue, options);
374
+ }
375
+
376
+ /**
377
+ * Internal: Compare two values based on search options
378
+ * @param {*} actual - The actual value from the object
379
+ * @param {*} expected - The expected value from the query
380
+ * @param {Object} options - Comparison options
381
+ * @returns {boolean} True if values match according to options
382
+ */
383
+ _compareValues(actual, expected, options) {
384
+ const { exact, caseSensitive } = options;
385
+ if (exact) {
386
+ if (
387
+ typeof actual === 'string' &&
388
+ typeof expected === 'string' &&
389
+ !caseSensitive
390
+ ) {
391
+ return actual.toLowerCase() === expected.toLowerCase();
392
+ }
393
+ return actual === expected;
394
+ }
395
+
396
+ // Partial matching for strings
397
+ if (
398
+ typeof actual === 'string' &&
399
+ typeof expected === 'string'
400
+ ) {
401
+ const a = caseSensitive ? actual : actual.toLowerCase();
402
+ const e = caseSensitive ? expected : expected.toLowerCase();
403
+ return a.includes(e);
404
+ }
405
+
406
+ return actual === expected;
407
+ }
408
+
409
+ /**
410
+ * Search through config data like a database query.
411
+ * Supports nested object searching and various matching options.
412
+ *
413
+ * @param {Object} query - Query object with key-value pairs to match
414
+ * @param {Object} [opts={}] - Search options
415
+ * @param {boolean} [opts.global=false] - Search in global config if true, project config if false
416
+ * @param {string} [opts.filename] - Custom filename for project searches
417
+ * @param {string} [opts.pathHash] - Hash for namespaced global config searches
418
+ * @param {boolean} [opts.exact=true] - Use exact matching (false for partial string matching)
419
+ * @param {boolean} [opts.caseSensitive=true] - Case sensitive string comparisons
420
+ * @param {string} [opts.searchIn] - Specific top-level key to search within (optional)
421
+ *
422
+ * @returns {Array} Array of matching objects/values with their keys
423
+ */
424
+ searchObject(query, opts = {}) {
425
+ const {
426
+ global = false,
427
+ filename,
428
+ pathHash,
429
+ exact = true,
430
+ caseSensitive = true,
431
+ searchIn,
432
+ } = opts;
433
+
434
+ let configData = global
435
+ ? this.read(null, { global: true, pathHash })
436
+ : this.read(null, { global: false, filename });
437
+
438
+ if (searchIn && configData[searchIn]) {
439
+ configData = { [searchIn]: configData[searchIn] };
440
+ }
441
+
442
+ const results = [];
443
+ const recurse = (obj, keyPath = '') => {
444
+ if (obj === null || obj === undefined) return;
445
+ if (typeof obj === 'object' && this._matchesQuery(obj, query, { exact, caseSensitive })) {
446
+ results.push({ key: keyPath, value: obj, matches: query });
447
+ }
448
+ if (typeof obj === 'object') {
449
+ if (Array.isArray(obj)) {
450
+ obj.forEach((item, idx) => recurse(item, `${keyPath}[${idx}]`));
451
+ } else {
452
+ Object.entries(obj).forEach(([k, v]) =>
453
+ recurse(v, keyPath ? `${keyPath}.${k}` : k)
454
+ );
455
+ }
456
+ }
457
+ };
458
+ recurse(configData);
459
+ return results;
460
+ }
461
+
462
+ /**
463
+ * Convenience method for simple key-value searches
464
+ * @param {string} key - The key to search for
465
+ * @param {*} value - The value to match
466
+ * @param {Object} [opts={}] - Same options as searchObject
467
+ * @returns {Array} Array of matching results
468
+ */
469
+ findByKeyValue(key, value, opts = {}) {
470
+ return this.searchObject({ [key]: value }, opts);
471
+ }
472
+
473
+ /**
474
+ * Search for objects containing any of the specified values
475
+ * @param {Object} query - Query object with key-value pairs (OR logic)
476
+ * @param {Object} [opts={}] - Same options as searchObject
477
+ * @returns {Array} Array of matching results
478
+ */
479
+ searchObjectAny(query, opts = {}) {
480
+ const {
481
+ global = false,
482
+ filename,
483
+ pathHash,
484
+ exact = true,
485
+ caseSensitive = true,
486
+ searchIn,
487
+ } = opts;
488
+
489
+ let configData = global
490
+ ? this.read(null, { global: true, pathHash })
491
+ : this.read(null, { global: false, filename });
492
+
493
+ if (searchIn && configData[searchIn]) {
494
+ configData = { [searchIn]: configData[searchIn] };
495
+ }
496
+
497
+ const results = [];
498
+ const recurseAny = (obj, keyPath = '') => {
499
+ if (obj === null || obj === undefined) return;
500
+ if (typeof obj === 'object') {
501
+ const matched = Object.entries(query).filter(([qk, qv]) =>
502
+ this._hasMatchingValue(obj, qk, qv, { exact, caseSensitive })
503
+ );
504
+ if (matched.length) {
505
+ results.push({
506
+ key: keyPath,
507
+ value: obj,
508
+ matches: Object.fromEntries(matched),
509
+ });
510
+ }
511
+ }
512
+ if (typeof obj === 'object') {
513
+ if (Array.isArray(obj)) {
514
+ obj.forEach((item, idx) => recurseAny(item, `${keyPath}[${idx}]`));
515
+ } else {
516
+ Object.entries(obj).forEach(([k, v]) =>
517
+ recurseAny(v, keyPath ? `${keyPath}.${k}` : k)
518
+ );
519
+ }
520
+ }
521
+ };
522
+ recurseAny(configData);
523
+ return results;
524
+ }
525
+ }
526
+
527
+ export default Config;