gsd-opencode 1.9.2 → 1.10.1
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/agents/gsd-debugger.md +5 -5
- package/bin/gsd-install.js +105 -0
- package/bin/gsd.js +352 -0
- package/{command → commands}/gsd/add-phase.md +1 -1
- package/{command → commands}/gsd/audit-milestone.md +1 -1
- package/{command → commands}/gsd/debug.md +3 -3
- package/{command → commands}/gsd/discuss-phase.md +1 -1
- package/{command → commands}/gsd/execute-phase.md +1 -1
- package/{command → commands}/gsd/list-phase-assumptions.md +1 -1
- package/{command → commands}/gsd/map-codebase.md +1 -1
- package/{command → commands}/gsd/new-milestone.md +1 -1
- package/{command → commands}/gsd/new-project.md +3 -3
- package/{command → commands}/gsd/plan-phase.md +2 -2
- package/{command → commands}/gsd/research-phase.md +1 -1
- package/{command → commands}/gsd/verify-work.md +1 -1
- package/get-shit-done/workflows/list-phase-assumptions.md +1 -1
- package/get-shit-done/workflows/verify-work.md +5 -5
- package/lib/constants.js +193 -0
- package/package.json +34 -20
- package/src/commands/check.js +329 -0
- package/src/commands/config.js +337 -0
- package/src/commands/install.js +608 -0
- package/src/commands/list.js +256 -0
- package/src/commands/repair.js +519 -0
- package/src/commands/uninstall.js +732 -0
- package/src/commands/update.js +444 -0
- package/src/services/backup-manager.js +585 -0
- package/src/services/config.js +262 -0
- package/src/services/file-ops.js +830 -0
- package/src/services/health-checker.js +475 -0
- package/src/services/manifest-manager.js +301 -0
- package/src/services/migration-service.js +831 -0
- package/src/services/repair-service.js +846 -0
- package/src/services/scope-manager.js +303 -0
- package/src/services/settings.js +553 -0
- package/src/services/structure-detector.js +240 -0
- package/src/services/update-service.js +863 -0
- package/src/utils/hash.js +71 -0
- package/src/utils/interactive.js +222 -0
- package/src/utils/logger.js +128 -0
- package/src/utils/npm-registry.js +255 -0
- package/src/utils/path-resolver.js +226 -0
- /package/{command → commands}/gsd/add-todo.md +0 -0
- /package/{command → commands}/gsd/check-todos.md +0 -0
- /package/{command → commands}/gsd/complete-milestone.md +0 -0
- /package/{command → commands}/gsd/help.md +0 -0
- /package/{command → commands}/gsd/insert-phase.md +0 -0
- /package/{command → commands}/gsd/pause-work.md +0 -0
- /package/{command → commands}/gsd/plan-milestone-gaps.md +0 -0
- /package/{command → commands}/gsd/progress.md +0 -0
- /package/{command → commands}/gsd/quick.md +0 -0
- /package/{command → commands}/gsd/remove-phase.md +0 -0
- /package/{command → commands}/gsd/resume-work.md +0 -0
- /package/{command → commands}/gsd/set-model.md +0 -0
- /package/{command → commands}/gsd/set-profile.md +0 -0
- /package/{command → commands}/gsd/settings.md +0 -0
- /package/{command → commands}/gsd/update.md +0 -0
- /package/{command → commands}/gsd/whats-new.md +0 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings manager for persistent user configuration.
|
|
3
|
+
*
|
|
4
|
+
* This module provides user-level configuration management with XDG Base Directory
|
|
5
|
+
* compliance, atomic file writes, dot-notation key access, and default value support.
|
|
6
|
+
* Unlike ConfigManager which tracks installation state, SettingsManager handles
|
|
7
|
+
* user preferences like default installation scope, UI settings, and behavior options.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - XDG Base Directory specification compliance (~/.config/gsd-opencode/settings.json)
|
|
11
|
+
* - Atomic writes using temp-then-rename pattern to prevent corruption
|
|
12
|
+
* - Dot-notation key access (e.g., 'ui.colors' → config.ui.colors)
|
|
13
|
+
* - Default values merged with user overrides
|
|
14
|
+
* - In-memory caching for performance
|
|
15
|
+
*
|
|
16
|
+
* @module settings
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs/promises';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import os from 'os';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration value type.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {string|number|boolean|Object|null} ConfigValue
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Manages user configuration with XDG-compliant storage and atomic writes.
|
|
31
|
+
*
|
|
32
|
+
* This class provides a complete settings management solution with persistent
|
|
33
|
+
* storage, dot-notation key access, and safe atomic file operations. Settings
|
|
34
|
+
* are stored in ~/.config/gsd-opencode/settings.json following the XDG Base
|
|
35
|
+
* Directory specification.
|
|
36
|
+
*
|
|
37
|
+
* @class SettingsManager
|
|
38
|
+
* @example
|
|
39
|
+
* const settings = new SettingsManager();
|
|
40
|
+
*
|
|
41
|
+
* // Get a value (returns default if not set)
|
|
42
|
+
* const colors = await settings.get('ui.colors'); // true
|
|
43
|
+
*
|
|
44
|
+
* // Set a value using dot-notation
|
|
45
|
+
* await settings.set('ui.colors', false);
|
|
46
|
+
*
|
|
47
|
+
* // Reset to default
|
|
48
|
+
* await settings.reset('ui.colors');
|
|
49
|
+
*
|
|
50
|
+
* // List all settings (merged with defaults)
|
|
51
|
+
* const all = await settings.list();
|
|
52
|
+
*
|
|
53
|
+
* // Get config file path
|
|
54
|
+
* console.log(settings.getConfigPath()); // ~/.config/gsd-opencode/settings.json
|
|
55
|
+
*/
|
|
56
|
+
export class SettingsManager {
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new SettingsManager instance.
|
|
59
|
+
*
|
|
60
|
+
* Initializes the configuration directory path following the XDG Base Directory
|
|
61
|
+
* specification: uses XDG_CONFIG_HOME environment variable if set, otherwise
|
|
62
|
+
* defaults to ~/.config. Creates the config directory on first write.
|
|
63
|
+
*
|
|
64
|
+
* Default values are applied when a key is not present in user configuration:
|
|
65
|
+
* - 'install.defaultScope': 'global' - Default installation scope
|
|
66
|
+
* - 'ui.colors': true - Enable colored output
|
|
67
|
+
* - 'ui.progressBars': true - Show progress indicators
|
|
68
|
+
* - 'behavior.confirmDestructive': true - Confirm before destructive operations
|
|
69
|
+
* - 'logging.verbose': false - Verbose logging disabled by default
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const settings = new SettingsManager();
|
|
73
|
+
*
|
|
74
|
+
* // With custom config directory (for testing)
|
|
75
|
+
* process.env.XDG_CONFIG_HOME = '/tmp/test-config';
|
|
76
|
+
* const testSettings = new SettingsManager();
|
|
77
|
+
*/
|
|
78
|
+
constructor() {
|
|
79
|
+
// Follow XDG Base Directory Specification
|
|
80
|
+
// Priority: XDG_CONFIG_HOME env var > ~/.config
|
|
81
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
82
|
+
const baseDir = xdgConfig || path.join(os.homedir(), '.config');
|
|
83
|
+
|
|
84
|
+
this.configDir = path.join(baseDir, 'opencode', 'gsd-opencode');
|
|
85
|
+
this.configPath = path.join(this.configDir, 'settings.json');
|
|
86
|
+
|
|
87
|
+
// Default configuration values
|
|
88
|
+
// These are used when a key is not present in user configuration
|
|
89
|
+
// Stored as nested object to match user config structure
|
|
90
|
+
this.defaults = {
|
|
91
|
+
install: {
|
|
92
|
+
defaultScope: 'global'
|
|
93
|
+
},
|
|
94
|
+
ui: {
|
|
95
|
+
colors: true,
|
|
96
|
+
progressBars: true
|
|
97
|
+
},
|
|
98
|
+
behavior: {
|
|
99
|
+
confirmDestructive: true
|
|
100
|
+
},
|
|
101
|
+
logging: {
|
|
102
|
+
verbose: false
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// In-memory cache for performance
|
|
107
|
+
// Cache is invalidated on any write operation
|
|
108
|
+
this._cache = null;
|
|
109
|
+
this._cacheValid = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Gets a configuration value by key.
|
|
114
|
+
*
|
|
115
|
+
* Retrieves the value for the specified key using dot-notation (e.g., 'ui.colors').
|
|
116
|
+
* If the key is not found in user configuration, returns the default value.
|
|
117
|
+
* Returns undefined if the key has no user value and no default.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} key - Configuration key using dot-notation (e.g., 'ui.colors')
|
|
120
|
+
* @returns {Promise<ConfigValue>} Configuration value, default value, or undefined
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const settings = new SettingsManager();
|
|
124
|
+
*
|
|
125
|
+
* // Get with default fallback
|
|
126
|
+
* const colors = await settings.get('ui.colors'); // true (default)
|
|
127
|
+
*
|
|
128
|
+
* await settings.set('ui.colors', false);
|
|
129
|
+
* const updated = await settings.get('ui.colors'); // false (user set)
|
|
130
|
+
*
|
|
131
|
+
* // Get nested value
|
|
132
|
+
* await settings.set('user.name', 'John');
|
|
133
|
+
* const name = await settings.get('user.name'); // 'John'
|
|
134
|
+
*/
|
|
135
|
+
async get(key) {
|
|
136
|
+
if (!key || typeof key !== 'string') {
|
|
137
|
+
throw new Error('Key must be a non-empty string');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const config = await this._load();
|
|
141
|
+
const value = this._getNested(config, key);
|
|
142
|
+
|
|
143
|
+
// Return user value if set, otherwise return default
|
|
144
|
+
return value !== undefined ? value : this._getNested(this.defaults, key);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sets a configuration value by key.
|
|
149
|
+
*
|
|
150
|
+
* Stores the value for the specified key using dot-notation. Creates nested
|
|
151
|
+
* objects as needed. Performs an atomic write to prevent config corruption
|
|
152
|
+
* if the process crashes during the write operation.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} key - Configuration key using dot-notation (e.g., 'ui.colors')
|
|
155
|
+
* @param {ConfigValue} value - Value to store
|
|
156
|
+
* @returns {Promise<void>}
|
|
157
|
+
* @throws {Error} If the config file cannot be written
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const settings = new SettingsManager();
|
|
161
|
+
*
|
|
162
|
+
* // Set simple value
|
|
163
|
+
* await settings.set('ui.colors', false);
|
|
164
|
+
*
|
|
165
|
+
* // Set nested value (creates intermediate objects)
|
|
166
|
+
* await settings.set('user.preferences.theme', 'dark');
|
|
167
|
+
*
|
|
168
|
+
* // Set boolean
|
|
169
|
+
* await settings.set('behavior.confirmDestructive', false);
|
|
170
|
+
*
|
|
171
|
+
* // Set number
|
|
172
|
+
* await settings.set('output.timeout', 30);
|
|
173
|
+
*/
|
|
174
|
+
async set(key, value) {
|
|
175
|
+
if (!key || typeof key !== 'string') {
|
|
176
|
+
throw new Error('Key must be a non-empty string');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const config = await this._load();
|
|
180
|
+
this._setNested(config, key, value);
|
|
181
|
+
await this._save(config);
|
|
182
|
+
|
|
183
|
+
// Update cache with new config
|
|
184
|
+
this._cache = config;
|
|
185
|
+
this._cacheValid = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Resets configuration to defaults.
|
|
190
|
+
*
|
|
191
|
+
* If a specific key is provided, removes that key from user configuration
|
|
192
|
+
* so the default value will be used on next access. If no key is provided,
|
|
193
|
+
* deletes the entire configuration file, effectively resetting all settings
|
|
194
|
+
* to their defaults.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} [key] - Specific key to reset, or omit to reset all
|
|
197
|
+
* @returns {Promise<void>}
|
|
198
|
+
* @throws {Error} If the config file cannot be modified
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* const settings = new SettingsManager();
|
|
202
|
+
*
|
|
203
|
+
* // Reset specific key to default
|
|
204
|
+
* await settings.set('ui.colors', false);
|
|
205
|
+
* await settings.reset('ui.colors');
|
|
206
|
+
* const colors = await settings.get('ui.colors'); // true (default)
|
|
207
|
+
*
|
|
208
|
+
* // Reset all settings to defaults
|
|
209
|
+
* await settings.reset();
|
|
210
|
+
* const all = await settings.list(); // All defaults
|
|
211
|
+
*/
|
|
212
|
+
async reset(key) {
|
|
213
|
+
if (key) {
|
|
214
|
+
// Reset specific key
|
|
215
|
+
const config = await this._load();
|
|
216
|
+
this._deleteNested(config, key);
|
|
217
|
+
|
|
218
|
+
// If config is now empty, delete the file entirely
|
|
219
|
+
if (Object.keys(config).length === 0) {
|
|
220
|
+
await this._deleteConfigFile();
|
|
221
|
+
} else {
|
|
222
|
+
await this._save(config);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// Reset all - delete config file
|
|
226
|
+
await this._deleteConfigFile();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Invalidate cache
|
|
230
|
+
this._cacheValid = false;
|
|
231
|
+
this._cache = null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Lists all configuration values.
|
|
236
|
+
*
|
|
237
|
+
* Returns all configuration settings merged with their default values.
|
|
238
|
+
* User-set values override defaults. The returned object contains the
|
|
239
|
+
* fully resolved configuration.
|
|
240
|
+
*
|
|
241
|
+
* @returns {Promise<Object>} All configuration values merged with defaults
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* const settings = new SettingsManager();
|
|
245
|
+
*
|
|
246
|
+
* const all = await settings.list();
|
|
247
|
+
* // {
|
|
248
|
+
* // install: { defaultScope: 'global' },
|
|
249
|
+
* // ui: { colors: true, progressBars: true },
|
|
250
|
+
* // behavior: { confirmDestructive: true },
|
|
251
|
+
* // logging: { verbose: false }
|
|
252
|
+
* // }
|
|
253
|
+
*/
|
|
254
|
+
async list() {
|
|
255
|
+
const config = await this._load();
|
|
256
|
+
return this._deepMerge({}, this._flattenDefaults(), config);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Gets the raw user configuration without defaults.
|
|
261
|
+
*
|
|
262
|
+
* Returns only the user-set configuration values, without merging defaults.
|
|
263
|
+
* Useful for debugging or when you need to see exactly what the user has
|
|
264
|
+
* configured versus what defaults would apply.
|
|
265
|
+
*
|
|
266
|
+
* @returns {Promise<Object>} User configuration only (no defaults)
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* const settings = new SettingsManager();
|
|
270
|
+
*
|
|
271
|
+
* // Before setting any values
|
|
272
|
+
* const raw = await settings.getRaw(); // {}
|
|
273
|
+
*
|
|
274
|
+
* await settings.set('ui.colors', false);
|
|
275
|
+
* const updated = await settings.getRaw(); // { ui: { colors: false } }
|
|
276
|
+
*/
|
|
277
|
+
async getRaw() {
|
|
278
|
+
return this._load();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Gets the absolute path to the configuration file.
|
|
283
|
+
*
|
|
284
|
+
* Returns the full filesystem path where settings are stored. This follows
|
|
285
|
+
* the XDG Base Directory specification and respects the XDG_CONFIG_HOME
|
|
286
|
+
* environment variable.
|
|
287
|
+
*
|
|
288
|
+
* @returns {string} Absolute path to settings.json
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* const settings = new SettingsManager();
|
|
292
|
+
*
|
|
293
|
+
* console.log(settings.getConfigPath());
|
|
294
|
+
* // /home/user/.config/gsd-opencode/settings.json
|
|
295
|
+
* // or if XDG_CONFIG_HOME is set:
|
|
296
|
+
* // /custom/path/gsd-opencode/settings.json
|
|
297
|
+
*/
|
|
298
|
+
getConfigPath() {
|
|
299
|
+
return this.configPath;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Loads configuration from disk.
|
|
304
|
+
*
|
|
305
|
+
* Private method that reads and parses the configuration file. Uses an
|
|
306
|
+
* in-memory cache to avoid repeated disk reads. Returns an empty object
|
|
307
|
+
* if the file doesn't exist yet.
|
|
308
|
+
*
|
|
309
|
+
* @returns {Promise<Object>} Parsed configuration object
|
|
310
|
+
* @private
|
|
311
|
+
* @throws {Error} If the config file contains invalid JSON
|
|
312
|
+
*/
|
|
313
|
+
async _load() {
|
|
314
|
+
// Return cached config if valid
|
|
315
|
+
if (this._cacheValid && this._cache) {
|
|
316
|
+
return this._cache;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const content = await fs.readFile(this.configPath, 'utf-8');
|
|
321
|
+
const config = JSON.parse(content);
|
|
322
|
+
|
|
323
|
+
// Update cache
|
|
324
|
+
this._cache = config;
|
|
325
|
+
this._cacheValid = true;
|
|
326
|
+
|
|
327
|
+
return config;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error.code === 'ENOENT') {
|
|
330
|
+
// Config file doesn't exist yet - return empty object
|
|
331
|
+
this._cache = {};
|
|
332
|
+
this._cacheValid = true;
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (error instanceof SyntaxError) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Invalid configuration file at ${this.configPath}: ${error.message}. ` +
|
|
339
|
+
'The file may be corrupted. You can reset it with: gsd-opencode config reset --all'
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (error.code === 'EACCES') {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`Permission denied: Cannot read configuration file at ${this.configPath}. ` +
|
|
346
|
+
'Check file permissions or run with appropriate privileges.'
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Saves configuration to disk atomically.
|
|
356
|
+
*
|
|
357
|
+
* Private method that writes configuration using the atomic write pattern:
|
|
358
|
+
* 1. Write to a temporary file
|
|
359
|
+
* 2. Rename temp file to final location
|
|
360
|
+
*
|
|
361
|
+
* This ensures that the configuration file is never in a partially written
|
|
362
|
+
* state, even if the process crashes during the write operation.
|
|
363
|
+
*
|
|
364
|
+
* @param {Object} config - Configuration object to save
|
|
365
|
+
* @returns {Promise<void>}
|
|
366
|
+
* @private
|
|
367
|
+
* @throws {Error} If the config file cannot be written
|
|
368
|
+
*/
|
|
369
|
+
async _save(config) {
|
|
370
|
+
// Ensure config directory exists
|
|
371
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
372
|
+
|
|
373
|
+
// Atomic write: write to temp file, then rename
|
|
374
|
+
// This prevents corruption if process crashes during write
|
|
375
|
+
const tempPath = `${this.configPath}.tmp.${Date.now()}`;
|
|
376
|
+
const content = JSON.stringify(config, null, 2);
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
380
|
+
await fs.rename(tempPath, this.configPath);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
// Clean up temp file on error
|
|
383
|
+
try {
|
|
384
|
+
await fs.unlink(tempPath);
|
|
385
|
+
} catch {
|
|
386
|
+
// Ignore cleanup errors
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (error.code === 'EACCES') {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Permission denied: Cannot write configuration to ${this.configDir}. ` +
|
|
392
|
+
'Check directory permissions or run with appropriate privileges.'
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (error.code === 'ENOSPC') {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`No space left on device: Cannot write configuration to ${this.configDir}. ` +
|
|
399
|
+
'Free up disk space and try again.'
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
throw new Error(`Failed to save configuration: ${error.message}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Deletes the configuration file.
|
|
409
|
+
*
|
|
410
|
+
* Private helper that removes the configuration file entirely.
|
|
411
|
+
* Silently succeeds if the file doesn't exist.
|
|
412
|
+
*
|
|
413
|
+
* @returns {Promise<void>}
|
|
414
|
+
* @private
|
|
415
|
+
*/
|
|
416
|
+
async _deleteConfigFile() {
|
|
417
|
+
try {
|
|
418
|
+
await fs.unlink(this.configPath);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error.code !== 'ENOENT') {
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
// File doesn't exist - that's fine
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Gets a nested value using dot-notation key.
|
|
429
|
+
*
|
|
430
|
+
* Private helper that traverses an object using a dot-separated key path.
|
|
431
|
+
* Returns undefined if any part of the path doesn't exist.
|
|
432
|
+
*
|
|
433
|
+
* @param {Object} obj - Object to traverse
|
|
434
|
+
* @param {string} key - Dot-notation key (e.g., 'ui.colors')
|
|
435
|
+
* @returns {ConfigValue} Value at the key path, or undefined
|
|
436
|
+
* @private
|
|
437
|
+
*/
|
|
438
|
+
_getNested(obj, key) {
|
|
439
|
+
const keys = key.split('.');
|
|
440
|
+
let result = obj;
|
|
441
|
+
|
|
442
|
+
for (const k of keys) {
|
|
443
|
+
if (result === null || result === undefined) {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
result = result[k];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Sets a nested value using dot-notation key.
|
|
454
|
+
*
|
|
455
|
+
* Private helper that creates intermediate objects as needed and sets
|
|
456
|
+
* the value at the specified key path. Modifies the object in place.
|
|
457
|
+
*
|
|
458
|
+
* @param {Object} obj - Object to modify
|
|
459
|
+
* @param {string} key - Dot-notation key (e.g., 'ui.colors')
|
|
460
|
+
* @param {ConfigValue} value - Value to set
|
|
461
|
+
* @private
|
|
462
|
+
*/
|
|
463
|
+
_setNested(obj, key, value) {
|
|
464
|
+
const keys = key.split('.');
|
|
465
|
+
const last = keys.pop();
|
|
466
|
+
let target = obj;
|
|
467
|
+
|
|
468
|
+
// Create intermediate objects as needed
|
|
469
|
+
for (const k of keys) {
|
|
470
|
+
if (!(k in target) || typeof target[k] !== 'object' || target[k] === null) {
|
|
471
|
+
target[k] = {};
|
|
472
|
+
}
|
|
473
|
+
target = target[k];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
target[last] = value;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Deletes a nested value using dot-notation key.
|
|
481
|
+
*
|
|
482
|
+
* Private helper that removes a key from a nested object structure.
|
|
483
|
+
* Silently returns if any part of the path doesn't exist.
|
|
484
|
+
*
|
|
485
|
+
* @param {Object} obj - Object to modify
|
|
486
|
+
* @param {string} key - Dot-notation key (e.g., 'ui.colors')
|
|
487
|
+
* @private
|
|
488
|
+
*/
|
|
489
|
+
_deleteNested(obj, key) {
|
|
490
|
+
const keys = key.split('.');
|
|
491
|
+
const last = keys.pop();
|
|
492
|
+
let target = obj;
|
|
493
|
+
|
|
494
|
+
// Traverse to parent of target key
|
|
495
|
+
for (const k of keys) {
|
|
496
|
+
if (!(k in target)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
target = target[k];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
delete target[last];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Deep merges multiple objects.
|
|
507
|
+
*
|
|
508
|
+
* Private helper that recursively merges source objects into the target.
|
|
509
|
+
* Later sources overwrite earlier ones. Creates new objects for nested
|
|
510
|
+
* merges to avoid mutating sources.
|
|
511
|
+
*
|
|
512
|
+
* @param {Object} target - Target object to merge into
|
|
513
|
+
* @param {...Object} sources - Source objects to merge from
|
|
514
|
+
* @returns {Object} Merged object (same reference as target)
|
|
515
|
+
* @private
|
|
516
|
+
*/
|
|
517
|
+
_deepMerge(target, ...sources) {
|
|
518
|
+
for (const source of sources) {
|
|
519
|
+
for (const key in source) {
|
|
520
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
521
|
+
target[key] = this._deepMerge(target[key] || {}, source[key]);
|
|
522
|
+
} else {
|
|
523
|
+
target[key] = source[key];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return target;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Returns a copy of default configuration.
|
|
532
|
+
*
|
|
533
|
+
* Private helper that returns a deep copy of the default configuration
|
|
534
|
+
* for merging with user config.
|
|
535
|
+
*
|
|
536
|
+
* @returns {Object} Deep copy of defaults
|
|
537
|
+
* @private
|
|
538
|
+
*/
|
|
539
|
+
_flattenDefaults() {
|
|
540
|
+
return this._deepMerge({}, this.defaults);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Default export for the settings module.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* import { SettingsManager } from './services/settings.js';
|
|
549
|
+
* const settings = new SettingsManager();
|
|
550
|
+
*/
|
|
551
|
+
export default {
|
|
552
|
+
SettingsManager
|
|
553
|
+
};
|