coding-tool-x 3.7.11 → 3.7.13
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 +9 -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 +53 -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 +17 -4
- package/src/server/services/channel-health.js +5 -0
- package/src/server/services/channel-sync-utils.js +1 -1
- package/src/server/services/model-definition-schema.js +2 -1
- package/src/server/services/omp-channels.js +213 -26
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const { execFileSync } = require('child_process');
|
|
@@ -15,6 +16,7 @@ const {
|
|
|
15
16
|
} = require('./omp-settings-manager');
|
|
16
17
|
const {
|
|
17
18
|
createSkippedResult,
|
|
19
|
+
isLocalProxyBaseUrl,
|
|
18
20
|
resolveApiKeyValue,
|
|
19
21
|
resolveExistingActiveChannel,
|
|
20
22
|
upsertSyncedChannels
|
|
@@ -22,6 +24,24 @@ const {
|
|
|
22
24
|
|
|
23
25
|
const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh|off)$/;
|
|
24
26
|
|
|
27
|
+
function clearOmpChannelBalanceCache(channel) {
|
|
28
|
+
try {
|
|
29
|
+
require('./channel-balance').clearChannelBalanceCache('omp', channel);
|
|
30
|
+
} catch (_) {
|
|
31
|
+
// Balance cache invalidation is an optimization; channel activation must still succeed.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function selectLatestEnabledChannel(channels = []) {
|
|
36
|
+
const enabledChannels = (channels || []).filter(channel => channel?.enabled !== false);
|
|
37
|
+
if (enabledChannels.length === 0) return null;
|
|
38
|
+
return enabledChannels.reduce((latest, current) => {
|
|
39
|
+
const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
|
|
40
|
+
const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
|
|
41
|
+
return currentTs > latestTs ? current : latest;
|
|
42
|
+
}, enabledChannels[0]);
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
function loadManagedOmpModeState() {
|
|
26
46
|
ensureStorageDirMigrated();
|
|
27
47
|
try {
|
|
@@ -56,6 +76,54 @@ function writeManagedOmpModeState(filePath, state) {
|
|
|
56
76
|
}
|
|
57
77
|
}
|
|
58
78
|
|
|
79
|
+
function getOmpGatewaySecretPath() {
|
|
80
|
+
return PATHS.ompGatewaySecret
|
|
81
|
+
|| path.join(path.dirname(PATHS.activeChannel.omp), 'omp-gateway-secret');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeOmpGatewaySecret(filePath, secret) {
|
|
85
|
+
const dir = path.dirname(filePath);
|
|
86
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
88
|
+
try {
|
|
89
|
+
fs.writeFileSync(temporaryPath, `${secret}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
90
|
+
fs.chmodSync(temporaryPath, 0o600);
|
|
91
|
+
fs.renameSync(temporaryPath, filePath);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getOrCreateOmpGatewaySecret() {
|
|
99
|
+
ensureStorageDirMigrated();
|
|
100
|
+
const filePath = getOmpGatewaySecretPath();
|
|
101
|
+
const previousStateSecret = loadManagedOmpModeState()?.gateway?.secret;
|
|
102
|
+
const fileExists = fs.existsSync(filePath);
|
|
103
|
+
|
|
104
|
+
if (fileExists) {
|
|
105
|
+
try {
|
|
106
|
+
const stored = fs.readFileSync(filePath, 'utf8').trim();
|
|
107
|
+
if (stored) return stored;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (previousStateSecret) {
|
|
110
|
+
writeOmpGatewaySecret(filePath, previousStateSecret);
|
|
111
|
+
return previousStateSecret;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Failed to read persisted OMP gateway secret: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
if (!previousStateSecret) {
|
|
116
|
+
throw new Error('Persisted OMP gateway secret is empty');
|
|
117
|
+
}
|
|
118
|
+
writeOmpGatewaySecret(filePath, previousStateSecret);
|
|
119
|
+
return previousStateSecret;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const secret = String(previousStateSecret || crypto.randomBytes(32).toString('hex'));
|
|
123
|
+
writeOmpGatewaySecret(filePath, secret);
|
|
124
|
+
return secret;
|
|
125
|
+
}
|
|
126
|
+
|
|
59
127
|
function enableManagedOmpMode(activeChannelId = null, gateway = null) {
|
|
60
128
|
ensureStorageDirMigrated();
|
|
61
129
|
const filePath = PATHS.activeChannel.omp;
|
|
@@ -140,30 +208,33 @@ class OmpChannelService extends BaseChannelService {
|
|
|
140
208
|
gateway: state.gateway,
|
|
141
209
|
activeChannelId: channel.id
|
|
142
210
|
} : {});
|
|
211
|
+
return;
|
|
143
212
|
}
|
|
213
|
+
writeManagedOmpProviders([channel], {});
|
|
144
214
|
}
|
|
145
215
|
|
|
146
216
|
_onAfterCreate(_channel, allChannels) {
|
|
147
|
-
this.
|
|
217
|
+
this.syncOmpProvidersForCurrentMode(allChannels);
|
|
148
218
|
}
|
|
149
219
|
|
|
150
220
|
_onAfterUpdate(_oldChannel, _newChannel, allChannels) {
|
|
151
|
-
this.
|
|
221
|
+
this.syncOmpProvidersForCurrentMode(allChannels);
|
|
152
222
|
}
|
|
153
223
|
|
|
154
224
|
_onAfterDelete(_channel, allChannels) {
|
|
155
|
-
this.
|
|
225
|
+
this.syncOmpProvidersForCurrentMode(allChannels);
|
|
156
226
|
}
|
|
157
227
|
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
228
|
+
syncOmpProvidersForCurrentMode(channels = this.getChannels().channels) {
|
|
229
|
+
if (isManagedOmpModeEnabled()) {
|
|
230
|
+
const state = loadManagedOmpModeState();
|
|
231
|
+
return this.syncManagedOmpProviders(channels, state?.gateway ? {
|
|
232
|
+
gateway: state.gateway,
|
|
233
|
+
activeChannelId: state.activeChannelId
|
|
234
|
+
} : {});
|
|
161
235
|
}
|
|
162
|
-
const
|
|
163
|
-
return this.syncManagedOmpProviders(
|
|
164
|
-
gateway: state.gateway,
|
|
165
|
-
activeChannelId: state.activeChannelId
|
|
166
|
-
} : {});
|
|
236
|
+
const activeChannel = selectLatestEnabledChannel(channels);
|
|
237
|
+
return this.syncManagedOmpProviders(activeChannel ? [activeChannel] : [], {});
|
|
167
238
|
}
|
|
168
239
|
|
|
169
240
|
syncManagedOmpProviders(channels = this.getChannels().channels, options = {}) {
|
|
@@ -181,8 +252,45 @@ class OmpChannelService extends BaseChannelService {
|
|
|
181
252
|
return getLastManagedOmpSyncResult();
|
|
182
253
|
}
|
|
183
254
|
|
|
255
|
+
activateStaticOmpChannel(channelId) {
|
|
256
|
+
const data = this.loadChannels();
|
|
257
|
+
const channel = data.channels.find(item => item.id === channelId);
|
|
258
|
+
if (!channel) {
|
|
259
|
+
throw new Error('Channel not found');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const previousData = JSON.parse(JSON.stringify(data));
|
|
263
|
+
const wasEnabled = channel.enabled !== false;
|
|
264
|
+
data.channels.forEach(item => {
|
|
265
|
+
item.enabled = item.id === channelId;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
this.saveChannels(data);
|
|
270
|
+
const sync = this.syncManagedOmpProviders([channel], {});
|
|
271
|
+
if (!wasEnabled) {
|
|
272
|
+
clearOmpChannelBalanceCache(channel);
|
|
273
|
+
}
|
|
274
|
+
return { channel, sync };
|
|
275
|
+
} catch (error) {
|
|
276
|
+
try {
|
|
277
|
+
this.saveChannels(previousData);
|
|
278
|
+
} catch (rollbackError) {
|
|
279
|
+
error.rollbackError = rollbackError;
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
applyChannelToSettings(channelId) {
|
|
286
|
+
if (isManagedOmpModeEnabled()) {
|
|
287
|
+
return super.applyChannelToSettings(channelId);
|
|
288
|
+
}
|
|
289
|
+
return this.activateStaticOmpChannel(channelId).channel;
|
|
290
|
+
}
|
|
291
|
+
|
|
184
292
|
syncManagedProviderExtension(channels = this.getChannels().channels) {
|
|
185
|
-
return this.
|
|
293
|
+
return this.syncOmpProvidersForCurrentMode(channels);
|
|
186
294
|
}
|
|
187
295
|
|
|
188
296
|
disableManagedProviderExtension() {
|
|
@@ -209,6 +317,24 @@ function getOriginalOmpProviderId(providerId = '') {
|
|
|
209
317
|
return normalized.startsWith('ctx-') ? normalized.slice(4) : normalized;
|
|
210
318
|
}
|
|
211
319
|
|
|
320
|
+
function isManagedOmpProviderId(providerId = '') {
|
|
321
|
+
return String(providerId || '').trim().startsWith('ctx-');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isGeneratedOmpApiKey(value = '') {
|
|
325
|
+
return /^ctx_[a-f0-9]{40}$/i.test(String(value || '').trim());
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isGeneratedOmpBaseUrl(value = '') {
|
|
329
|
+
const raw = String(value || '').trim();
|
|
330
|
+
if (!isLocalProxyBaseUrl(raw)) return false;
|
|
331
|
+
try {
|
|
332
|
+
return /^\/omp\/[a-f0-9]{24}(?:\/|$)/i.test(new URL(raw).pathname);
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
212
338
|
function normalizeOmpModelId(value = '') {
|
|
213
339
|
return String(value || '').trim().replace(OMP_THINKING_SUFFIX_RE, '');
|
|
214
340
|
}
|
|
@@ -297,6 +423,28 @@ function collectOmpSyncSelections(modelsConfig = {}, settings = {}) {
|
|
|
297
423
|
return [...selections.values()];
|
|
298
424
|
}
|
|
299
425
|
|
|
426
|
+
function findOmpOriginalProvider(providers = {}, providerId = '') {
|
|
427
|
+
const normalizedProviderId = normalizeProviderId(getOriginalOmpProviderId(providerId));
|
|
428
|
+
const candidates = Object.entries(providers)
|
|
429
|
+
.filter(([key]) => !isManagedOmpProviderId(key))
|
|
430
|
+
.map(([id, provider]) => ({
|
|
431
|
+
id,
|
|
432
|
+
provider,
|
|
433
|
+
normalizedId: normalizeProviderId(id)
|
|
434
|
+
}));
|
|
435
|
+
const exactMatches = candidates.filter(item => item.normalizedId === normalizedProviderId);
|
|
436
|
+
if (exactMatches.length === 1) return exactMatches[0];
|
|
437
|
+
if (exactMatches.length > 1) return null;
|
|
438
|
+
|
|
439
|
+
const prefixMatches = candidates
|
|
440
|
+
.filter(item => normalizedProviderId.startsWith(`${item.normalizedId}-`))
|
|
441
|
+
.sort((left, right) => right.normalizedId.length - left.normalizedId.length);
|
|
442
|
+
if (prefixMatches.length === 0) return null;
|
|
443
|
+
const longestLength = prefixMatches[0].normalizedId.length;
|
|
444
|
+
const longestMatches = prefixMatches.filter(item => item.normalizedId.length === longestLength);
|
|
445
|
+
return longestMatches.length === 1 ? longestMatches[0] : null;
|
|
446
|
+
}
|
|
447
|
+
|
|
300
448
|
function findOmpExistingChannel(channels = [], providerId = '', baseUrl = '') {
|
|
301
449
|
const rawProvider = String(providerId || '').trim();
|
|
302
450
|
const originalProvider = getOriginalOmpProviderId(rawProvider);
|
|
@@ -386,9 +534,18 @@ function readOmpApiKeyCredential(providerId = '') {
|
|
|
386
534
|
return { value: '', source: '', envName: '' };
|
|
387
535
|
}
|
|
388
536
|
|
|
537
|
+
function resolveOmpAuthMode(provider = {}, sourceProvider = {}, existing = null) {
|
|
538
|
+
const rawAuth = String(sourceProvider.auth || provider.auth || '').trim().toLowerCase();
|
|
539
|
+
if (rawAuth === 'none') return 'none';
|
|
540
|
+
if (existing?.authMode === 'none') return 'none';
|
|
541
|
+
if (existing?.authMode === 'oauth') return 'oauth';
|
|
542
|
+
return 'api_key';
|
|
543
|
+
}
|
|
544
|
+
|
|
389
545
|
function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
546
|
+
const providers = getOmpProviders(modelsConfig);
|
|
390
547
|
const providerId = selection?.providerId || '';
|
|
391
|
-
const provider =
|
|
548
|
+
const provider = providers[providerId] || null;
|
|
392
549
|
if (!provider || typeof provider !== 'object') {
|
|
393
550
|
return {
|
|
394
551
|
skip: true,
|
|
@@ -396,14 +553,42 @@ function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
|
396
553
|
};
|
|
397
554
|
}
|
|
398
555
|
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
556
|
+
const managedProvider = isManagedOmpProviderId(providerId);
|
|
557
|
+
const originalProviderEntry = managedProvider
|
|
558
|
+
? findOmpOriginalProvider(providers, providerId)
|
|
559
|
+
: null;
|
|
560
|
+
const sourceProvider = originalProviderEntry?.provider || (managedProvider ? {} : provider);
|
|
561
|
+
const originalProviderId = originalProviderEntry?.id || getOriginalOmpProviderId(providerId);
|
|
562
|
+
const providerBaseUrl = String(sourceProvider.baseUrl || sourceProvider.base_url || '').trim();
|
|
563
|
+
const managedBaseUrl = String(provider.baseUrl || provider.base_url || '').trim();
|
|
564
|
+
const existing = findOmpExistingChannel(
|
|
565
|
+
channels,
|
|
566
|
+
originalProviderId || providerId,
|
|
567
|
+
managedProvider ? '' : managedBaseUrl
|
|
568
|
+
);
|
|
569
|
+
const baseUrl = managedProvider && (
|
|
570
|
+
!providerBaseUrl || isGeneratedOmpBaseUrl(providerBaseUrl)
|
|
571
|
+
)
|
|
572
|
+
? existing?.baseUrl || ''
|
|
573
|
+
: providerBaseUrl || existing?.baseUrl || managedBaseUrl;
|
|
574
|
+
const authMode = resolveOmpAuthMode(provider, sourceProvider, existing);
|
|
575
|
+
|
|
576
|
+
let credential = resolveApiKeyValue(sourceProvider.apiKey || sourceProvider.api_key || '');
|
|
403
577
|
if (!credential.value) {
|
|
404
|
-
credential = readOmpApiKeyCredential(providerId);
|
|
578
|
+
credential = readOmpApiKeyCredential(originalProviderId || providerId);
|
|
405
579
|
}
|
|
406
|
-
const
|
|
580
|
+
const existingCredential = resolveApiKeyValue(existing?.apiKey || '');
|
|
581
|
+
const apiKey = credential.value && !isGeneratedOmpApiKey(credential.value)
|
|
582
|
+
? credential.value
|
|
583
|
+
: existingCredential.value && !isGeneratedOmpApiKey(existingCredential.value)
|
|
584
|
+
? existingCredential.value
|
|
585
|
+
: '';
|
|
586
|
+
if (!credential.value || isGeneratedOmpApiKey(credential.value)) {
|
|
587
|
+
credential = existingCredential.value && !isGeneratedOmpApiKey(existingCredential.value)
|
|
588
|
+
? { ...existingCredential, source: 'existing-channel' }
|
|
589
|
+
: credential;
|
|
590
|
+
}
|
|
591
|
+
|
|
407
592
|
const providerModelIds = collectOmpModelIds(provider);
|
|
408
593
|
const selectedModelIds = Array.isArray(selection?.modelIds) ? selection.modelIds : [];
|
|
409
594
|
const allowedModels = selectedModelIds.length > 0 ? selectedModelIds : providerModelIds;
|
|
@@ -412,11 +597,11 @@ function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
|
412
597
|
|| existing?.model
|
|
413
598
|
|| allowedModels[0]
|
|
414
599
|
|| null;
|
|
415
|
-
if (!apiKey) {
|
|
600
|
+
if (!baseUrl || (authMode !== 'none' && !apiKey)) {
|
|
416
601
|
return {
|
|
417
602
|
skip: true,
|
|
418
603
|
channel: existing || null,
|
|
419
|
-
warning: `OMP 当前 provider "${providerId}"
|
|
604
|
+
warning: `OMP 当前 provider "${providerId}" 缺少可解析的上游 Base URL 或 API Key,OAuth/登录态渠道不支持同步导入。`
|
|
420
605
|
};
|
|
421
606
|
}
|
|
422
607
|
|
|
@@ -424,11 +609,11 @@ function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
|
424
609
|
name: existing?.name || originalProviderId || providerId,
|
|
425
610
|
providerKey: originalProviderId || providerId,
|
|
426
611
|
baseUrl,
|
|
427
|
-
apiKey,
|
|
428
|
-
providerApi: normalizeProviderApi(provider.api),
|
|
429
|
-
wireApi: provider.api || 'openai-completions',
|
|
430
|
-
authMode
|
|
431
|
-
oauthProviderId: '',
|
|
612
|
+
apiKey: authMode === 'none' ? '' : apiKey,
|
|
613
|
+
providerApi: normalizeProviderApi(sourceProvider.api || provider.api),
|
|
614
|
+
wireApi: sourceProvider.api || provider.api || 'openai-completions',
|
|
615
|
+
authMode,
|
|
616
|
+
oauthProviderId: authMode === 'oauth' ? (existing?.oauthProviderId || originalProviderId || providerId) : '',
|
|
432
617
|
model: preferredModel,
|
|
433
618
|
allowedModels,
|
|
434
619
|
models: filterOmpProviderModels(provider.models, allowedModels),
|
|
@@ -486,6 +671,7 @@ module.exports = {
|
|
|
486
671
|
getEnabledChannels: () => service.getEnabledChannels(),
|
|
487
672
|
saveChannelOrder: (order) => service.saveChannelOrder(order),
|
|
488
673
|
applyChannelToSettings: (id) => service.applyChannelToSettings(id),
|
|
674
|
+
activateStaticOmpChannel: (id) => service.activateStaticOmpChannel(id),
|
|
489
675
|
disableAllChannels: () => service.disableAllChannels(),
|
|
490
676
|
getEffectiveApiKey: (channel) => channel?.apiKey || null,
|
|
491
677
|
syncManagedOmpProviders: (channels, options) => service.syncManagedOmpProviders(channels, options),
|
|
@@ -493,6 +679,7 @@ module.exports = {
|
|
|
493
679
|
syncManagedProviderExtension: (channels) => service.syncManagedProviderExtension(channels),
|
|
494
680
|
disableManagedProviderExtension: () => service.disableManagedProviderExtension(),
|
|
495
681
|
isManagedOmpModeEnabled,
|
|
682
|
+
getOrCreateOmpGatewaySecret,
|
|
496
683
|
enableManagedOmpMode,
|
|
497
684
|
disableManagedOmpMode,
|
|
498
685
|
loadManagedOmpModeState,
|
|
@@ -5,6 +5,11 @@ const yaml = require('js-yaml');
|
|
|
5
5
|
const { HOME_DIR, PATHS = {} } = require('../../config/paths');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_OMP_COMMAND = 'omp';
|
|
8
|
+
const YAML_DUMP_OPTIONS = Object.freeze({
|
|
9
|
+
lineWidth: 120,
|
|
10
|
+
noRefs: true,
|
|
11
|
+
sortKeys: false
|
|
12
|
+
});
|
|
8
13
|
|
|
9
14
|
function expandHome(input = '') {
|
|
10
15
|
const value = String(input || '').trim();
|
|
@@ -43,7 +48,9 @@ function getDefaultOmpAgentDir(env = process.env) {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
function getFallbackOmpAgentDir(runtime, env = process.env) {
|
|
46
|
-
const configured = expandHome(
|
|
51
|
+
const configured = expandHome(
|
|
52
|
+
env.PI_CODING_AGENT_DIR || env.OMP_CODING_AGENT_DIR || ''
|
|
53
|
+
);
|
|
47
54
|
if (configured) return configured;
|
|
48
55
|
return getDefaultOmpAgentDir(env);
|
|
49
56
|
}
|
|
@@ -121,6 +128,14 @@ function ensureOmpDir(dirPath) {
|
|
|
121
128
|
}
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
function isPlainObject(value) {
|
|
132
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const prototype = Object.getPrototypeOf(value);
|
|
136
|
+
return prototype === Object.prototype || prototype === null;
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
function readJsonFile(filePath, fallback = {}) {
|
|
125
140
|
try {
|
|
126
141
|
if (!fs.existsSync(filePath)) return fallback;
|
|
@@ -150,11 +165,7 @@ function readYamlFile(filePath, fallback = {}) {
|
|
|
150
165
|
|
|
151
166
|
function writeYamlFile(filePath, data) {
|
|
152
167
|
ensureOmpDir(path.dirname(filePath));
|
|
153
|
-
fs.writeFileSync(filePath, yaml.dump(data || {},
|
|
154
|
-
lineWidth: 120,
|
|
155
|
-
noRefs: true,
|
|
156
|
-
sortKeys: false
|
|
157
|
-
}), 'utf8');
|
|
168
|
+
fs.writeFileSync(filePath, yaml.dump(data || {}, YAML_DUMP_OPTIONS), 'utf8');
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
function readOmpSettings() {
|
|
@@ -166,6 +177,58 @@ function writeOmpSettings(settings) {
|
|
|
166
177
|
writeYamlFile(getOmpPaths().settings, settings || {});
|
|
167
178
|
}
|
|
168
179
|
|
|
180
|
+
function readOmpSettingsStrict() {
|
|
181
|
+
const filePath = getOmpPaths().settings;
|
|
182
|
+
let source;
|
|
183
|
+
try {
|
|
184
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error?.code === 'ENOENT') return {};
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const settings = yaml.load(source);
|
|
191
|
+
if (!isPlainObject(settings)) {
|
|
192
|
+
throw new Error('Invalid OMP config');
|
|
193
|
+
}
|
|
194
|
+
return settings;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function writeOmpSettingsAtomic(settings) {
|
|
198
|
+
const filePath = getOmpPaths().settings;
|
|
199
|
+
const directory = path.dirname(filePath);
|
|
200
|
+
ensureOmpDir(directory);
|
|
201
|
+
let mode = 0o600;
|
|
202
|
+
try {
|
|
203
|
+
mode = fs.statSync(filePath).mode & 0o777;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
206
|
+
}
|
|
207
|
+
const temporaryPath = path.join(
|
|
208
|
+
directory,
|
|
209
|
+
`.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
fs.writeFileSync(temporaryPath, yaml.dump(settings, YAML_DUMP_OPTIONS), {
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
flag: 'wx',
|
|
216
|
+
mode
|
|
217
|
+
});
|
|
218
|
+
fs.chmodSync(temporaryPath, mode);
|
|
219
|
+
fs.renameSync(temporaryPath, filePath);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
try {
|
|
222
|
+
if (fs.existsSync(temporaryPath)) {
|
|
223
|
+
fs.unlinkSync(temporaryPath);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Preserve the original write, chmod, or rename failure.
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
169
232
|
function getOmpCommand(env = process.env) {
|
|
170
233
|
return String(env.OMP_COMMAND || DEFAULT_OMP_COMMAND).trim() || DEFAULT_OMP_COMMAND;
|
|
171
234
|
}
|
|
@@ -286,6 +349,8 @@ module.exports = {
|
|
|
286
349
|
writeYamlFile,
|
|
287
350
|
readOmpSettings,
|
|
288
351
|
writeOmpSettings,
|
|
352
|
+
readOmpSettingsStrict,
|
|
353
|
+
writeOmpSettingsAtomic,
|
|
289
354
|
getOmpCommand,
|
|
290
355
|
getOmpProfile,
|
|
291
356
|
resolveOmpRuntime,
|