@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.
- package/LICENSE +25 -25
- package/README.md +1166 -1166
- package/actions/autopublish.old.js +293 -293
- package/actions/config.js +182 -182
- package/actions/create.js +466 -466
- package/actions/help.js +164 -164
- package/actions/iris/buildStage.js +874 -874
- package/actions/iris/delete.js +256 -256
- package/actions/iris/dev.js +391 -391
- package/actions/iris/index.js +6 -6
- package/actions/iris/link.js +375 -375
- package/actions/iris/recover.js +268 -268
- package/actions/main.js +80 -80
- package/actions/publish.js +1420 -1420
- package/actions/pull.js +684 -684
- package/actions/setup.js +148 -148
- package/actions/status.js +17 -17
- package/actions/update.js +248 -248
- package/bin/magentrix.js +393 -393
- package/package.json +55 -55
- package/utils/assetPaths.js +158 -158
- package/utils/autopublishLock.js +77 -77
- package/utils/cacher.js +206 -206
- package/utils/cli/checkInstanceUrl.js +76 -74
- package/utils/cli/helpers/compare.js +282 -282
- package/utils/cli/helpers/ensureApiKey.js +63 -63
- package/utils/cli/helpers/ensureCredentials.js +68 -68
- package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
- package/utils/cli/writeRecords.js +262 -262
- package/utils/compare.js +135 -135
- package/utils/compress.js +17 -17
- package/utils/config.js +527 -527
- package/utils/debug.js +144 -144
- package/utils/diagnostics/testPublishLogic.js +96 -96
- package/utils/diff.js +49 -49
- package/utils/downloadAssets.js +291 -291
- package/utils/filetag.js +115 -115
- package/utils/hash.js +14 -14
- package/utils/iris/backup.js +411 -411
- package/utils/iris/builder.js +541 -541
- package/utils/iris/config-reader.js +664 -664
- package/utils/iris/deleteHelper.js +150 -150
- package/utils/iris/errors.js +537 -537
- package/utils/iris/linker.js +601 -601
- package/utils/iris/lock.js +360 -360
- package/utils/iris/validation.js +360 -360
- package/utils/iris/validator.js +281 -281
- package/utils/iris/zipper.js +248 -248
- package/utils/logger.js +291 -291
- package/utils/magentrix/api/assets.js +220 -220
- package/utils/magentrix/api/auth.js +107 -107
- package/utils/magentrix/api/createEntity.js +61 -61
- package/utils/magentrix/api/deleteEntity.js +55 -55
- package/utils/magentrix/api/iris.js +251 -251
- package/utils/magentrix/api/meqlQuery.js +36 -36
- package/utils/magentrix/api/retrieveEntity.js +86 -86
- package/utils/magentrix/api/updateEntity.js +66 -66
- package/utils/magentrix/fetch.js +168 -168
- package/utils/merge.js +22 -22
- package/utils/permissionError.js +70 -70
- package/utils/preferences.js +40 -40
- package/utils/progress.js +469 -469
- package/utils/spinner.js +43 -43
- package/utils/template.js +52 -52
- package/utils/updateFileBase.js +121 -121
- package/utils/workspaces.js +108 -108
- package/vars/config.js +11 -11
- 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;
|