@magentrix-corp/magentrix-cli 1.0.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 (43) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +471 -0
  3. package/actions/autopublish.js +283 -0
  4. package/actions/autopublish.old.js +293 -0
  5. package/actions/autopublish.v2.js +447 -0
  6. package/actions/create.js +329 -0
  7. package/actions/help.js +165 -0
  8. package/actions/main.js +81 -0
  9. package/actions/publish.js +567 -0
  10. package/actions/pull.js +139 -0
  11. package/actions/setup.js +61 -0
  12. package/actions/status.js +17 -0
  13. package/bin/magentrix.js +159 -0
  14. package/package.json +61 -0
  15. package/utils/cacher.js +112 -0
  16. package/utils/cli/checkInstanceUrl.js +29 -0
  17. package/utils/cli/helpers/compare.js +281 -0
  18. package/utils/cli/helpers/ensureApiKey.js +57 -0
  19. package/utils/cli/helpers/ensureCredentials.js +60 -0
  20. package/utils/cli/helpers/ensureInstanceUrl.js +63 -0
  21. package/utils/cli/writeRecords.js +223 -0
  22. package/utils/compare.js +135 -0
  23. package/utils/compress.js +18 -0
  24. package/utils/config.js +451 -0
  25. package/utils/diff.js +49 -0
  26. package/utils/downloadAssets.js +75 -0
  27. package/utils/filetag.js +115 -0
  28. package/utils/hash.js +14 -0
  29. package/utils/magentrix/api/assets.js +145 -0
  30. package/utils/magentrix/api/auth.js +56 -0
  31. package/utils/magentrix/api/createEntity.js +61 -0
  32. package/utils/magentrix/api/deleteEntity.js +55 -0
  33. package/utils/magentrix/api/meqlQuery.js +31 -0
  34. package/utils/magentrix/api/retrieveEntity.js +32 -0
  35. package/utils/magentrix/api/updateEntity.js +66 -0
  36. package/utils/magentrix/fetch.js +154 -0
  37. package/utils/merge.js +22 -0
  38. package/utils/preferences.js +40 -0
  39. package/utils/spinner.js +43 -0
  40. package/utils/template.js +52 -0
  41. package/utils/updateFileBase.js +103 -0
  42. package/vars/config.js +1 -0
  43. package/vars/global.js +33 -0
@@ -0,0 +1,135 @@
1
+ import fs from 'fs';
2
+ import { sha256 } from './hash.js';
3
+ import { findFileByTag } from './filetag.js';
4
+ import path from 'path';
5
+
6
+ /**
7
+ * Compares a local file to its remote representation and determines their sync status.
8
+ *
9
+ * - Checks file existence, last modified times, and content hashes.
10
+ * - Returns detailed sync info; never logs or warns directly.
11
+ *
12
+ * @param {string} localFilePath - The path to the local file.
13
+ * @param {Object} remoteData - Object containing remote file metadata and content.
14
+ * @param {string} remoteData.content - The file content from the remote.
15
+ * @param {string} remoteData.ModifiedOn - The last modified date of the remote file (ISO string).
16
+ * @returns {Object} An object describing the sync status. Possible statuses:
17
+ * - 'in_sync': Files match (hashes and times).
18
+ * - 'behind': Local file is older than remote, but content is identical.
19
+ * - 'conflict': Local file is behind remote and contents differ (conflict).
20
+ * - 'ahead': Local file is newer than remote and contents differ.
21
+ * - 'ahead_identical': Local file is newer than remote but contents are identical (possible clock drift).
22
+ * - 'content_differs': Timestamps match but contents differ (possible manual edit).
23
+ * - 'missing': Local file does not exist (safe to download from remote).
24
+ * Additional returned fields depend on status.
25
+ */
26
+ export function compareLocalAndRemote(localFilePath, remoteData) {
27
+ // If local file does not exist, it's fine—just return 'missing'
28
+ if (!fs.existsSync(localFilePath)) {
29
+ return {
30
+ status: 'missing',
31
+ message: 'Local file is missing.'
32
+ };
33
+ }
34
+
35
+ // Gather local file details
36
+ const localStats = fs.statSync(localFilePath);
37
+ const localContent = fs.readFileSync(localFilePath, 'utf8');
38
+ const localMtime = Math.ceil(localStats.mtimeMs); // Local last modified time (ms)
39
+ const localHash = sha256(localContent); // Local file content hash
40
+
41
+ // Gather remote file details
42
+ const remoteContent = remoteData.content;
43
+ const remoteMtime = Math.ceil(new Date(remoteData.ModifiedOn).getTime()); // Remote last modified time (ms)
44
+ const remoteHash = sha256(remoteContent); // Remote file content hash
45
+
46
+ // Case: local is behind remote (older mtime)
47
+ if (localMtime < remoteMtime) {
48
+ if (localHash !== remoteHash) {
49
+ // File changed on both local and remote: conflict!
50
+ return {
51
+ status: 'conflict',
52
+ localMtime,
53
+ remoteMtime,
54
+ localHash,
55
+ remoteHash,
56
+ message: 'Local file is behind remote and has conflicting changes.'
57
+ };
58
+ } else {
59
+ // Local is older but identical to remote
60
+ // return {
61
+ // status: 'behind',
62
+ // localMtime,
63
+ // remoteMtime,
64
+ // message: 'Local file is behind remote but contents are identical.'
65
+ // };
66
+
67
+ return {
68
+ status: 'in_sync',
69
+ localMtime,
70
+ remoteMtime,
71
+ message: 'Local and remote file are in sync.'
72
+ };
73
+ }
74
+ }
75
+ // Case: local is ahead of remote (newer mtime)
76
+ else if (localMtime > remoteMtime) {
77
+ if (localHash !== remoteHash) {
78
+ return {
79
+ status: 'ahead',
80
+ localMtime,
81
+ remoteMtime,
82
+ localHash,
83
+ remoteHash,
84
+ message: 'Local file is ahead of remote and content differs.'
85
+ };
86
+ } else {
87
+ // Local is ahead but identical
88
+ // return {
89
+ // status: 'ahead_identical',
90
+ // localMtime,
91
+ // remoteMtime,
92
+ // message: 'Local file is ahead of remote but contents are identical.'
93
+ // };
94
+
95
+ return {
96
+ status: 'in_sync',
97
+ localMtime,
98
+ remoteMtime,
99
+ message: 'Local and remote file are in sync.'
100
+ };
101
+ }
102
+ }
103
+ // Case: mtimes match
104
+ else {
105
+ // Check for rename
106
+ const matchingFilePath = findFileByTag(remoteData.Id);
107
+ const absoluteLocalPath = path.resolve(localFilePath);
108
+
109
+ if (matchingFilePath !== absoluteLocalPath) {
110
+ return {
111
+ status: 'rename',
112
+ localMtime,
113
+ remoteMtime,
114
+ message: 'Local and remote file names differ.'
115
+ };
116
+ } else if (localHash === remoteHash) {
117
+ return {
118
+ status: 'in_sync',
119
+ localMtime,
120
+ remoteMtime,
121
+ message: 'Local and remote file are in sync.'
122
+ };
123
+ } else {
124
+ // Timestamps match but contents do not
125
+ return {
126
+ status: 'content_differs',
127
+ localMtime,
128
+ remoteMtime,
129
+ localHash,
130
+ remoteHash,
131
+ message: 'File times match, but content differs (possible manual edit).'
132
+ };
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,18 @@
1
+ import pako from 'pako';
2
+
3
+ export const compressString = (input) => {
4
+ const compressed = pako.deflate(input);
5
+ const compressedStr = Buffer.from(compressed).toString('base64');
6
+ return compressedStr;
7
+ }
8
+
9
+ export const decompressString = (compressedInput) => {
10
+ try {
11
+ if (!compressedInput) return '';
12
+ const decoded = Buffer.from(compressedInput, 'base64');
13
+ const decompressed = pako.inflate(decoded, { to: 'string' });
14
+ return decompressed;
15
+ } catch (err) {
16
+ return '';
17
+ }
18
+ }
@@ -0,0 +1,451 @@
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
+ /**
205
+ * Save a key-value pair to config (global or project).
206
+ * If opts.pathHash is specified in global mode, data is stored under that subkey.
207
+ * For project saves, opts.filename can specify a custom filename.
208
+ * @param {string} key - The config key.
209
+ * @param {*} value - The value to store.
210
+ * @param {Object} opts
211
+ * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
212
+ * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
213
+ * @param {string} [opts.filename] - Optional. Custom filename for project saves (ignored for global).
214
+ */
215
+ save(key, value, opts = {}) {
216
+ const isGlobal = opts.global === true;
217
+ const filename = opts.filename;
218
+ const pathHash = opts.pathHash;
219
+
220
+ if (isGlobal) {
221
+ if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
222
+ if (pathHash) {
223
+ this._globalConfig[pathHash] = this._globalConfig[pathHash] || {};
224
+ this._globalConfig[pathHash][key] = value;
225
+ } else {
226
+ this._globalConfig[key] = value;
227
+ }
228
+ this._saveConfig(this._globalConfig, 'global');
229
+ } else {
230
+ if (filename) {
231
+ // Ensure project config folder exists
232
+ const projectFolderPath = path.join(this.projectDir, this.projectFolder);
233
+ if (!fs.existsSync(projectFolderPath)) {
234
+ fs.mkdirSync(projectFolderPath, { recursive: true, mode: 0o700 });
235
+ }
236
+ const customPath = path.join(projectFolderPath, filename);
237
+ // Load, update, and save the custom config file independently
238
+ const customConfig = this._loadConfig('project', customPath) || {};
239
+ customConfig[key] = value;
240
+ this._saveConfig(customConfig, 'project', customPath);
241
+ } else {
242
+ // Default project config
243
+ if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
244
+ this._projectConfig[key] = value;
245
+ this._saveConfig(this._projectConfig, 'project');
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Internal: Deep search through nested objects and arrays
252
+ * @param {*} obj - The object/value to search through
253
+ * @param {Object} query - The query object with key-value pairs to match
254
+ * @param {Object} [options={}] - Search options
255
+ * @param {boolean} [options.exact=true] - Whether to use exact matching or partial matching
256
+ * @param {boolean} [options.caseSensitive=true] - Whether string comparisons should be case sensitive
257
+ * @returns {boolean} True if the object matches the query
258
+ */
259
+ _matchesQuery(obj, query, options = {}) {
260
+ const { exact = true, caseSensitive = true } = options;
261
+ if (obj === null || obj === undefined) return false;
262
+ for (const [queryKey, expectedValue] of Object.entries(query)) {
263
+ if (!this._hasMatchingValue(obj, queryKey, expectedValue, options)) {
264
+ return false;
265
+ }
266
+ }
267
+ return true;
268
+ }
269
+
270
+ /**
271
+ * Internal: Check if an object has a matching value for a given key (supports nested keys)
272
+ * @param {*} obj - The object to search in
273
+ * @param {string} key - The key to search for (supports dot notation for nested keys)
274
+ * @param {*} expectedValue - The expected value
275
+ * @param {Object} options - Search options
276
+ * @returns {boolean} True if a matching value is found
277
+ */
278
+ _hasMatchingValue(obj, key, expectedValue, options) {
279
+ const { exact, caseSensitive } = options;
280
+ const keys = key.split('.');
281
+ let current = obj;
282
+
283
+ for (const k of keys) {
284
+ if (current === null || current === undefined) return false;
285
+
286
+ if (Array.isArray(current)) {
287
+ return current.some(item =>
288
+ this._hasMatchingValue(item, keys.slice(keys.indexOf(k)).join('.'), expectedValue, options)
289
+ );
290
+ } else if (typeof current === 'object') {
291
+ current = current[k];
292
+ } else {
293
+ return false;
294
+ }
295
+ }
296
+
297
+ return this._compareValues(current, expectedValue, options);
298
+ }
299
+
300
+ /**
301
+ * Internal: Compare two values based on search options
302
+ * @param {*} actual - The actual value from the object
303
+ * @param {*} expected - The expected value from the query
304
+ * @param {Object} options - Comparison options
305
+ * @returns {boolean} True if values match according to options
306
+ */
307
+ _compareValues(actual, expected, options) {
308
+ const { exact, caseSensitive } = options;
309
+ if (exact) {
310
+ if (
311
+ typeof actual === 'string' &&
312
+ typeof expected === 'string' &&
313
+ !caseSensitive
314
+ ) {
315
+ return actual.toLowerCase() === expected.toLowerCase();
316
+ }
317
+ return actual === expected;
318
+ }
319
+
320
+ // Partial matching for strings
321
+ if (
322
+ typeof actual === 'string' &&
323
+ typeof expected === 'string'
324
+ ) {
325
+ const a = caseSensitive ? actual : actual.toLowerCase();
326
+ const e = caseSensitive ? expected : expected.toLowerCase();
327
+ return a.includes(e);
328
+ }
329
+
330
+ return actual === expected;
331
+ }
332
+
333
+ /**
334
+ * Search through config data like a database query.
335
+ * Supports nested object searching and various matching options.
336
+ *
337
+ * @param {Object} query - Query object with key-value pairs to match
338
+ * @param {Object} [opts={}] - Search options
339
+ * @param {boolean} [opts.global=false] - Search in global config if true, project config if false
340
+ * @param {string} [opts.filename] - Custom filename for project searches
341
+ * @param {string} [opts.pathHash] - Hash for namespaced global config searches
342
+ * @param {boolean} [opts.exact=true] - Use exact matching (false for partial string matching)
343
+ * @param {boolean} [opts.caseSensitive=true] - Case sensitive string comparisons
344
+ * @param {string} [opts.searchIn] - Specific top-level key to search within (optional)
345
+ *
346
+ * @returns {Array} Array of matching objects/values with their keys
347
+ */
348
+ searchObject(query, opts = {}) {
349
+ const {
350
+ global = false,
351
+ filename,
352
+ pathHash,
353
+ exact = true,
354
+ caseSensitive = true,
355
+ searchIn,
356
+ } = opts;
357
+
358
+ let configData = global
359
+ ? this.read(null, { global: true, pathHash })
360
+ : this.read(null, { global: false, filename });
361
+
362
+ if (searchIn && configData[searchIn]) {
363
+ configData = { [searchIn]: configData[searchIn] };
364
+ }
365
+
366
+ const results = [];
367
+ const recurse = (obj, keyPath = '') => {
368
+ if (obj === null || obj === undefined) return;
369
+ if (typeof obj === 'object' && this._matchesQuery(obj, query, { exact, caseSensitive })) {
370
+ results.push({ key: keyPath, value: obj, matches: query });
371
+ }
372
+ if (typeof obj === 'object') {
373
+ if (Array.isArray(obj)) {
374
+ obj.forEach((item, idx) => recurse(item, `${keyPath}[${idx}]`));
375
+ } else {
376
+ Object.entries(obj).forEach(([k, v]) =>
377
+ recurse(v, keyPath ? `${keyPath}.${k}` : k)
378
+ );
379
+ }
380
+ }
381
+ };
382
+ recurse(configData);
383
+ return results;
384
+ }
385
+
386
+ /**
387
+ * Convenience method for simple key-value searches
388
+ * @param {string} key - The key to search for
389
+ * @param {*} value - The value to match
390
+ * @param {Object} [opts={}] - Same options as searchObject
391
+ * @returns {Array} Array of matching results
392
+ */
393
+ findByKeyValue(key, value, opts = {}) {
394
+ return this.searchObject({ [key]: value }, opts);
395
+ }
396
+
397
+ /**
398
+ * Search for objects containing any of the specified values
399
+ * @param {Object} query - Query object with key-value pairs (OR logic)
400
+ * @param {Object} [opts={}] - Same options as searchObject
401
+ * @returns {Array} Array of matching results
402
+ */
403
+ searchObjectAny(query, opts = {}) {
404
+ const {
405
+ global = false,
406
+ filename,
407
+ pathHash,
408
+ exact = true,
409
+ caseSensitive = true,
410
+ searchIn,
411
+ } = opts;
412
+
413
+ let configData = global
414
+ ? this.read(null, { global: true, pathHash })
415
+ : this.read(null, { global: false, filename });
416
+
417
+ if (searchIn && configData[searchIn]) {
418
+ configData = { [searchIn]: configData[searchIn] };
419
+ }
420
+
421
+ const results = [];
422
+ const recurseAny = (obj, keyPath = '') => {
423
+ if (obj === null || obj === undefined) return;
424
+ if (typeof obj === 'object') {
425
+ const matched = Object.entries(query).filter(([qk, qv]) =>
426
+ this._hasMatchingValue(obj, qk, qv, { exact, caseSensitive })
427
+ );
428
+ if (matched.length) {
429
+ results.push({
430
+ key: keyPath,
431
+ value: obj,
432
+ matches: Object.fromEntries(matched),
433
+ });
434
+ }
435
+ }
436
+ if (typeof obj === 'object') {
437
+ if (Array.isArray(obj)) {
438
+ obj.forEach((item, idx) => recurseAny(item, `${keyPath}[${idx}]`));
439
+ } else {
440
+ Object.entries(obj).forEach(([k, v]) =>
441
+ recurseAny(v, keyPath ? `${keyPath}.${k}` : k)
442
+ );
443
+ }
444
+ }
445
+ };
446
+ recurseAny(configData);
447
+ return results;
448
+ }
449
+ }
450
+
451
+ export default Config;
package/utils/diff.js ADDED
@@ -0,0 +1,49 @@
1
+ import { execSync, spawn } from 'child_process';
2
+ import chalk from 'chalk';
3
+
4
+ /**
5
+ * Checks if VS Code is installed and accessible via the CLI (`code` command).
6
+ * @returns {boolean} True if VS Code is available in PATH, false otherwise.
7
+ */
8
+ export const canOpenDiffInVSCode = () => {
9
+ try {
10
+ execSync('code --version', { stdio: 'ignore' });
11
+ return true;
12
+ } catch (err) {
13
+ return false;
14
+ }
15
+ };
16
+
17
+ /**
18
+ * Opens a side-by-side diff of two files in VS Code, if available.
19
+ * Falls back to terminal diff if VS Code is unavailable.
20
+ * @param {string} file1 - Path to the first file.
21
+ * @param {string} file2 - Path to the second file.
22
+ * @returns {boolean} True if VS Code diff was opened, false if fallback used.
23
+ */
24
+ export const openDiffInVSCode = (file1, file2) => {
25
+ if (!canOpenDiffInVSCode()) {
26
+ console.log(
27
+ chalk.yellow(
28
+ 'Warning: VS Code is not installed or the `code` command is not in your PATH.\n' +
29
+ 'Falling back to terminal diff.\nSee: https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line'
30
+ )
31
+ );
32
+ // Fallback: Use built-in diff (Unix) or fc (Windows)
33
+ try {
34
+ if (process.platform === 'win32') {
35
+ execSync(`fc "${file1}" "${file2}"`, { stdio: 'inherit' });
36
+ } else {
37
+ execSync(`diff -u "${file1}" "${file2}"`, { stdio: 'inherit' });
38
+ }
39
+ } catch (e) {
40
+ // Optionally handle diff exit code (e.g., files differ)
41
+ }
42
+ return false;
43
+ }
44
+
45
+ const child = spawn('code', ['--diff', file1, file2], { stdio: 'inherit', shell: true });
46
+ console.log(chalk.green('Opening diff in VS Code...'));
47
+
48
+ return true;
49
+ };