coding-tool-x 3.7.11 → 3.7.12
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/CHANGELOG.md +3 -0
- package/README.md +1 -1
- package/dist/web/assets/{Analytics-Ck_00Brj.js → Analytics-8dRv1aYW.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-Cpw2vUa8.js → ConfigTemplates-BQRXSICO.js} +1 -1
- package/dist/web/assets/Home-BtzWhKIa.js +1 -0
- package/dist/web/assets/Home-DYVy4cGZ.css +1 -0
- package/dist/web/assets/{PluginManager-2RlvEJCT.js → PluginManager-BTa-Ygf6.js} +1 -1
- package/dist/web/assets/{ProjectList-_ecCngpl.js → ProjectList-F_cB8ldA.js} +1 -1
- package/dist/web/assets/{SessionList-DlWcw3hO.js → SessionList-LOrbPQvu.js} +1 -1
- package/dist/web/assets/SkillManager-BsX8Qpj0.css +1 -0
- package/dist/web/assets/SkillManager-BzI_t-yV.js +1 -0
- package/dist/web/assets/{WorkspaceManager-3mqghi09.js → WorkspaceManager-Db9RGiz7.js} +1 -1
- package/dist/web/assets/index-DIWJmyL6.js +2 -0
- package/dist/web/assets/{index-BceXgSSe.css → index-bJRG3RXH.css} +1 -1
- package/dist/web/index.html +2 -2
- package/docs/superpowers/plans/2026-08-01-omp-skill-scan-settings.md +759 -0
- package/docs/superpowers/specs/2026-08-01-omp-skill-scan-settings-design.md +224 -0
- package/package.json +1 -1
- package/src/commands/proxy-control.js +12 -12
- package/src/commands/toggle-proxy.js +9 -9
- package/src/config/paths.js +52 -29
- package/src/index.js +1 -1
- package/src/server/api/plugins.js +119 -47
- package/src/server/api/skills.js +63 -10
- package/src/server/omp-proxy-server.js +14 -1
- package/src/server/services/model-definition-schema.js +2 -1
- package/src/server/services/omp-channels.js +71 -12
- package/src/server/services/omp-config.js +71 -6
- package/src/server/services/omp-native-plugin-adapter.js +456 -0
- package/src/server/services/omp-settings-manager.js +3 -1
- package/src/server/services/omp-skill-discovery.js +358 -0
- package/src/server/services/omp-skill-settings-service.js +97 -0
- package/src/server/services/pi-omp-migration.js +273 -0
- package/src/server/services/platform-resolution.js +44 -0
- package/src/server/services/plugins-service.js +85 -561
- package/src/server/services/project-path-validation.js +65 -0
- package/src/server/services/skill-service.js +169 -77
- package/dist/web/assets/Home-Bft3Kztl.js +0 -1
- package/dist/web/assets/Home-CHZJkkjH.css +0 -1
- package/dist/web/assets/SkillManager-CnY7IXkX.js +0 -1
- package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
- package/dist/web/assets/index-k5oP7c-A.js +0 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const {
|
|
5
|
+
getOmpCommand,
|
|
6
|
+
getOmpPaths,
|
|
7
|
+
readOmpSettings
|
|
8
|
+
} = require('./omp-config');
|
|
9
|
+
const { HOME_DIR, NATIVE_PATHS } = require('../../config/paths');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SKILL_SETTINGS = Object.freeze({
|
|
12
|
+
enabled: true,
|
|
13
|
+
enableCodexUser: true,
|
|
14
|
+
enableClaudeUser: true,
|
|
15
|
+
enableClaudeProject: true,
|
|
16
|
+
enablePiUser: true,
|
|
17
|
+
enablePiProject: true,
|
|
18
|
+
enableAgentsUser: true,
|
|
19
|
+
enableAgentsProject: true,
|
|
20
|
+
customDirectories: [],
|
|
21
|
+
ignoredSkills: [],
|
|
22
|
+
includeSkills: [],
|
|
23
|
+
disabledExtensions: []
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function expandHome(value) {
|
|
27
|
+
const input = String(value || '').trim();
|
|
28
|
+
if (input === '~') return HOME_DIR;
|
|
29
|
+
if (input.startsWith('~/')) return path.join(HOME_DIR, input.slice(2));
|
|
30
|
+
return input;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readEffectiveSettings(cwd) {
|
|
34
|
+
try {
|
|
35
|
+
const output = execFileSync(getOmpCommand(), ['config', 'list', '--json'], {
|
|
36
|
+
cwd: cwd || process.cwd(),
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
timeout: 5000,
|
|
40
|
+
windowsHide: true
|
|
41
|
+
});
|
|
42
|
+
const parsed = JSON.parse(String(output || '').trim());
|
|
43
|
+
const flattened = {};
|
|
44
|
+
for (const [key, item] of Object.entries(parsed || {})) {
|
|
45
|
+
if (key.startsWith('skills.')) {
|
|
46
|
+
flattened[key.slice('skills.'.length)] = item?.value;
|
|
47
|
+
} else if (key === 'disabledExtensions' || key === 'disabledProviders') {
|
|
48
|
+
flattened[key] = item?.value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { ...DEFAULT_SKILL_SETTINGS, ...flattened };
|
|
52
|
+
} catch {
|
|
53
|
+
const persisted = readOmpSettings();
|
|
54
|
+
return {
|
|
55
|
+
...DEFAULT_SKILL_SETTINGS,
|
|
56
|
+
...(persisted.skills && typeof persisted.skills === 'object' ? persisted.skills : {}),
|
|
57
|
+
disabledExtensions: persisted.disabledExtensions || [],
|
|
58
|
+
disabledProviders: persisted.disabledProviders || []
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function globMatches(value, pattern) {
|
|
64
|
+
const escaped = String(pattern || '')
|
|
65
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
66
|
+
.replace(/\*/g, '.*')
|
|
67
|
+
.replace(/\?/g, '.');
|
|
68
|
+
return new RegExp(`^${escaped}$`, 'i').test(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isIncluded(name, settings) {
|
|
72
|
+
const disabled = Array.isArray(settings.disabledExtensions)
|
|
73
|
+
? settings.disabledExtensions
|
|
74
|
+
: [];
|
|
75
|
+
if (disabled.some(item => item === `skill:${name}`)) return false;
|
|
76
|
+
|
|
77
|
+
const ignored = Array.isArray(settings.ignoredSkills) ? settings.ignoredSkills : [];
|
|
78
|
+
if (ignored.some(pattern => globMatches(name, pattern))) return false;
|
|
79
|
+
|
|
80
|
+
const included = Array.isArray(settings.includeSkills) ? settings.includeSkills : [];
|
|
81
|
+
return included.length === 0 || included.some(pattern => globMatches(name, pattern));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseSkill(service, skillFile, provider, scope, requireDescription) {
|
|
85
|
+
let metadata;
|
|
86
|
+
try {
|
|
87
|
+
metadata = service.parseSkillMd(fs.readFileSync(skillFile, 'utf8'));
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const directory = path.basename(path.dirname(skillFile));
|
|
93
|
+
const name = String(metadata.name || directory).trim();
|
|
94
|
+
const description = String(metadata.description || '').trim();
|
|
95
|
+
if (!name || (requireDescription && !description)) return null;
|
|
96
|
+
|
|
97
|
+
let realPath = skillFile;
|
|
98
|
+
try {
|
|
99
|
+
realPath = fs.realpathSync(skillFile);
|
|
100
|
+
} catch {
|
|
101
|
+
// Keep the absolute source path when a realpath cannot be resolved.
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const native = provider === 'native';
|
|
105
|
+
return {
|
|
106
|
+
key: `omp:${provider}:${scope}:${realPath}`,
|
|
107
|
+
name,
|
|
108
|
+
description,
|
|
109
|
+
directory,
|
|
110
|
+
installed: true,
|
|
111
|
+
isLocal: false,
|
|
112
|
+
source: native ? 'native-installed' : 'provider-installed',
|
|
113
|
+
sourceProvider: provider,
|
|
114
|
+
sourceScope: scope,
|
|
115
|
+
sourcePath: skillFile,
|
|
116
|
+
realPath,
|
|
117
|
+
readonly: !native,
|
|
118
|
+
shadowedSources: [],
|
|
119
|
+
protected: false,
|
|
120
|
+
readmeUrl: null,
|
|
121
|
+
repoOwner: null,
|
|
122
|
+
repoName: null,
|
|
123
|
+
repoBranch: null,
|
|
124
|
+
license: metadata.license || null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function scanOneLevel(service, root, descriptor, settings) {
|
|
129
|
+
if (!root || !fs.existsSync(root)) return [];
|
|
130
|
+
const result = [];
|
|
131
|
+
let entries = [];
|
|
132
|
+
try {
|
|
133
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
134
|
+
} catch {
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const entryPath = path.join(root, entry.name);
|
|
140
|
+
if (!entry.isDirectory()) {
|
|
141
|
+
if (!entry.isSymbolicLink()) continue;
|
|
142
|
+
try {
|
|
143
|
+
if (!fs.statSync(entryPath).isDirectory()) continue;
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const skillFile = path.join(entryPath, 'SKILL.md');
|
|
149
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
150
|
+
const skill = parseSkill(
|
|
151
|
+
service,
|
|
152
|
+
skillFile,
|
|
153
|
+
descriptor.provider,
|
|
154
|
+
descriptor.scope,
|
|
155
|
+
descriptor.requireDescription
|
|
156
|
+
);
|
|
157
|
+
if (skill && isIncluded(skill.name, settings)) {
|
|
158
|
+
result.push(skill);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getPluginInstallPaths(cwd) {
|
|
165
|
+
try {
|
|
166
|
+
const output = execFileSync(getOmpCommand(), ['plugin', 'list', '--json'], {
|
|
167
|
+
cwd: cwd || process.cwd(),
|
|
168
|
+
encoding: 'utf8',
|
|
169
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
170
|
+
timeout: 5000,
|
|
171
|
+
windowsHide: true
|
|
172
|
+
});
|
|
173
|
+
const parsed = JSON.parse(String(output || '').trim());
|
|
174
|
+
const paths = [];
|
|
175
|
+
const visit = (value, inheritedScope = 'user') => {
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
value.forEach(item => visit(item, inheritedScope));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!value || typeof value !== 'object') return;
|
|
181
|
+
const scope = value.scope || inheritedScope;
|
|
182
|
+
if (value.installPath || value.path) {
|
|
183
|
+
paths.push({
|
|
184
|
+
path: value.installPath || value.path,
|
|
185
|
+
scope
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
for (const nested of Object.values(value)) {
|
|
189
|
+
if (nested && typeof nested === 'object') visit(nested, scope);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
visit(parsed);
|
|
193
|
+
return paths;
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getClaudePluginSkillRoots() {
|
|
200
|
+
const pluginsDir = NATIVE_PATHS.claude.plugins
|
|
201
|
+
|| path.join(NATIVE_PATHS.claude.dir, 'plugins');
|
|
202
|
+
const installedFile = path.join(pluginsDir, 'installed_plugins.json');
|
|
203
|
+
if (!fs.existsSync(installedFile)) return [];
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(fs.readFileSync(installedFile, 'utf8'));
|
|
207
|
+
const roots = [];
|
|
208
|
+
for (const installations of Object.values(parsed?.plugins || {})) {
|
|
209
|
+
if (!Array.isArray(installations)) continue;
|
|
210
|
+
for (const installation of installations) {
|
|
211
|
+
const installPath = String(installation?.installPath || '').trim();
|
|
212
|
+
if (!installPath) continue;
|
|
213
|
+
roots.push({
|
|
214
|
+
path: path.join(installPath, 'skills'),
|
|
215
|
+
scope: installation.scope === 'project' ? 'project' : 'user'
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return roots;
|
|
220
|
+
} catch {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function providerEnabled(settings, provider, scope) {
|
|
226
|
+
if (Array.isArray(settings.disabledProviders) && settings.disabledProviders.includes(provider)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (provider === 'native') {
|
|
230
|
+
return scope === 'project' ? settings.enablePiProject !== false : settings.enablePiUser !== false;
|
|
231
|
+
}
|
|
232
|
+
if (provider === 'omp-plugins') {
|
|
233
|
+
return scope === 'project' ? settings.enablePiProject !== false : settings.enablePiUser !== false;
|
|
234
|
+
}
|
|
235
|
+
if (provider === 'claude') {
|
|
236
|
+
return scope === 'project' ? settings.enableClaudeProject !== false : settings.enableClaudeUser !== false;
|
|
237
|
+
}
|
|
238
|
+
if (provider === 'claude-plugins') {
|
|
239
|
+
return scope === 'project'
|
|
240
|
+
? settings.enableClaudeProject !== false
|
|
241
|
+
: settings.enableClaudeUser !== false;
|
|
242
|
+
}
|
|
243
|
+
if (provider === 'agents') {
|
|
244
|
+
return scope === 'project' ? settings.enableAgentsProject !== false : settings.enableAgentsUser !== false;
|
|
245
|
+
}
|
|
246
|
+
if (provider === 'codex' && scope === 'user') {
|
|
247
|
+
return settings.enableCodexUser !== false;
|
|
248
|
+
}
|
|
249
|
+
return [
|
|
250
|
+
settings.enablePiUser,
|
|
251
|
+
settings.enablePiProject,
|
|
252
|
+
settings.enableClaudeUser,
|
|
253
|
+
settings.enableClaudeProject,
|
|
254
|
+
settings.enableCodexUser,
|
|
255
|
+
settings.enableAgentsUser,
|
|
256
|
+
settings.enableAgentsProject
|
|
257
|
+
].some(value => value !== false);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function compactShadow(skill) {
|
|
261
|
+
return {
|
|
262
|
+
sourceProvider: skill.sourceProvider,
|
|
263
|
+
sourceScope: skill.sourceScope,
|
|
264
|
+
sourcePath: skill.sourcePath
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function deduplicateDiscoveredSkills(skills) {
|
|
269
|
+
const byName = new Map();
|
|
270
|
+
const byRealPath = new Map();
|
|
271
|
+
const result = [];
|
|
272
|
+
|
|
273
|
+
for (const skill of skills) {
|
|
274
|
+
const nameKey = skill.name.toLowerCase();
|
|
275
|
+
const pathKey = skill.realPath || skill.sourcePath;
|
|
276
|
+
const existing = byName.get(nameKey) || byRealPath.get(pathKey);
|
|
277
|
+
if (existing) {
|
|
278
|
+
existing.shadowedSources.push(compactShadow(skill));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
byName.set(nameKey, skill);
|
|
282
|
+
byRealPath.set(pathKey, skill);
|
|
283
|
+
result.push(skill);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function discoverOmpSkills(service, options = {}) {
|
|
290
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : null;
|
|
291
|
+
const settings = readEffectiveSettings(cwd);
|
|
292
|
+
if (settings.enabled === false) return [];
|
|
293
|
+
|
|
294
|
+
const ompPaths = getOmpPaths();
|
|
295
|
+
const userHome = HOME_DIR;
|
|
296
|
+
const descriptors = [
|
|
297
|
+
{ provider: 'native', scope: 'user', root: ompPaths.skills, requireDescription: true },
|
|
298
|
+
...(cwd ? [{ provider: 'native', scope: 'project', root: path.join(cwd, '.omp', 'skills'), requireDescription: true }] : []),
|
|
299
|
+
...getPluginInstallPaths(cwd)
|
|
300
|
+
.filter(item => cwd || item.scope !== 'project')
|
|
301
|
+
.map(item => ({
|
|
302
|
+
provider: 'omp-plugins',
|
|
303
|
+
scope: item.scope || 'user',
|
|
304
|
+
root: path.join(item.path, 'skills'),
|
|
305
|
+
requireDescription: true
|
|
306
|
+
})),
|
|
307
|
+
{ provider: 'claude', scope: 'user', root: NATIVE_PATHS.claude.skills, requireDescription: false },
|
|
308
|
+
...(cwd ? [{ provider: 'claude', scope: 'project', root: path.join(cwd, '.claude', 'skills'), requireDescription: false }] : []),
|
|
309
|
+
...getClaudePluginSkillRoots()
|
|
310
|
+
.filter(item => cwd || item.scope !== 'project')
|
|
311
|
+
.map(item => ({
|
|
312
|
+
provider: 'claude-plugins',
|
|
313
|
+
scope: item.scope,
|
|
314
|
+
root: item.path,
|
|
315
|
+
requireDescription: false
|
|
316
|
+
})),
|
|
317
|
+
{ provider: 'agents', scope: 'user', root: path.join(userHome, '.agents', 'skills'), requireDescription: false },
|
|
318
|
+
{ provider: 'agents', scope: 'user', root: path.join(userHome, '.agent', 'skills'), requireDescription: false },
|
|
319
|
+
...(cwd ? [
|
|
320
|
+
{ provider: 'agents', scope: 'project', root: path.join(cwd, '.agents', 'skills'), requireDescription: false },
|
|
321
|
+
{ provider: 'agents', scope: 'project', root: path.join(cwd, '.agent', 'skills'), requireDescription: false }
|
|
322
|
+
] : []),
|
|
323
|
+
{ provider: 'codex', scope: 'user', root: path.join(userHome, '.codex', 'skills'), requireDescription: false },
|
|
324
|
+
...(cwd ? [{ provider: 'codex', scope: 'project', root: path.join(cwd, '.codex', 'skills'), requireDescription: false }] : []),
|
|
325
|
+
{ provider: 'opencode', scope: 'user', root: path.join(NATIVE_PATHS.opencode.config, 'skills'), requireDescription: false },
|
|
326
|
+
...(cwd ? [{ provider: 'opencode', scope: 'project', root: path.join(cwd, '.opencode', 'skills'), requireDescription: false }] : [])
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
const discovered = [];
|
|
330
|
+
for (const descriptor of descriptors) {
|
|
331
|
+
if (!providerEnabled(settings, descriptor.provider, descriptor.scope)) continue;
|
|
332
|
+
discovered.push(...scanOneLevel(service, descriptor.root, descriptor, settings));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const customDirectories = Array.isArray(settings.customDirectories)
|
|
336
|
+
? settings.customDirectories
|
|
337
|
+
: [];
|
|
338
|
+
for (const customDirectory of customDirectories) {
|
|
339
|
+
const expanded = expandHome(customDirectory);
|
|
340
|
+
const root = path.isAbsolute(expanded)
|
|
341
|
+
? expanded
|
|
342
|
+
: path.resolve(cwd || process.cwd(), expanded);
|
|
343
|
+
discovered.push(...scanOneLevel(service, root, {
|
|
344
|
+
provider: 'custom',
|
|
345
|
+
scope: cwd && root.startsWith(`${cwd}${path.sep}`) ? 'project' : 'user',
|
|
346
|
+
requireDescription: true
|
|
347
|
+
}, settings));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return deduplicateDiscoveredSkills(discovered);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = {
|
|
354
|
+
DEFAULT_SKILL_SETTINGS,
|
|
355
|
+
discoverOmpSkills,
|
|
356
|
+
deduplicateDiscoveredSkills,
|
|
357
|
+
readEffectiveSettings
|
|
358
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const {
|
|
2
|
+
readOmpSettingsStrict,
|
|
3
|
+
writeOmpSettingsAtomic
|
|
4
|
+
} = require('./omp-config');
|
|
5
|
+
|
|
6
|
+
const OMP_SKILL_SETTING_DEFAULTS = Object.freeze({
|
|
7
|
+
enableCodexUser: true,
|
|
8
|
+
enableClaudeUser: true,
|
|
9
|
+
enablePiUser: true,
|
|
10
|
+
enablePiProject: true
|
|
11
|
+
});
|
|
12
|
+
const OMP_SKILL_SETTING_KEYS = Object.freeze(Object.keys(OMP_SKILL_SETTING_DEFAULTS));
|
|
13
|
+
const OMP_SKILL_SETTING_KEY_SET = new Set(OMP_SKILL_SETTING_KEYS);
|
|
14
|
+
|
|
15
|
+
function isPlainObject(value) {
|
|
16
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const prototype = Object.getPrototypeOf(value);
|
|
20
|
+
return prototype === Object.prototype || prototype === null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function selectOmpSkillSettings(skills) {
|
|
24
|
+
const source = skills || {};
|
|
25
|
+
const selected = {};
|
|
26
|
+
for (const key of OMP_SKILL_SETTING_KEYS) {
|
|
27
|
+
selected[key] = Object.prototype.hasOwnProperty.call(source, key)
|
|
28
|
+
? source[key]
|
|
29
|
+
: OMP_SKILL_SETTING_DEFAULTS[key];
|
|
30
|
+
}
|
|
31
|
+
return selected;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validatePersistedOmpSkillSettings(config) {
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(config, 'skills')) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
if (!isPlainObject(config.skills)) {
|
|
39
|
+
throw new Error('Invalid OMP config skills');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const key of OMP_SKILL_SETTING_KEYS) {
|
|
43
|
+
if (
|
|
44
|
+
Object.prototype.hasOwnProperty.call(config.skills, key) &&
|
|
45
|
+
typeof config.skills[key] !== 'boolean'
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(`Invalid OMP skill setting value for ${key}: expected boolean`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return config.skills;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateOmpSkillSettingsPatch(patch) {
|
|
54
|
+
if (!isPlainObject(patch)) {
|
|
55
|
+
throw new Error('Invalid OMP skill settings: expected an object');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
59
|
+
if (!OMP_SKILL_SETTING_KEY_SET.has(key)) {
|
|
60
|
+
throw new Error(`Invalid OMP skill setting: ${key}`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof value !== 'boolean') {
|
|
63
|
+
throw new Error(`Invalid OMP skill setting value for ${key}: expected boolean`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readOmpSkillSettings() {
|
|
69
|
+
const config = readOmpSettingsStrict();
|
|
70
|
+
return selectOmpSkillSettings(validatePersistedOmpSkillSettings(config));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function updateOmpSkillSettings(patch) {
|
|
74
|
+
validateOmpSkillSettingsPatch(patch);
|
|
75
|
+
|
|
76
|
+
const config = readOmpSettingsStrict();
|
|
77
|
+
const existingSkills = validatePersistedOmpSkillSettings(config);
|
|
78
|
+
if (Object.keys(patch).length === 0) {
|
|
79
|
+
return selectOmpSkillSettings(existingSkills);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const nextConfig = {
|
|
83
|
+
...config,
|
|
84
|
+
skills: {
|
|
85
|
+
...existingSkills,
|
|
86
|
+
...patch
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
writeOmpSettingsAtomic(nextConfig);
|
|
91
|
+
return selectOmpSkillSettings(nextConfig.skills);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
readOmpSkillSettings,
|
|
96
|
+
updateOmpSkillSettings
|
|
97
|
+
};
|