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.
Files changed (44) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/web/assets/{Analytics-Ck_00Brj.js → Analytics-8dRv1aYW.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-Cpw2vUa8.js → ConfigTemplates-BQRXSICO.js} +1 -1
  5. package/dist/web/assets/Home-BtzWhKIa.js +1 -0
  6. package/dist/web/assets/Home-DYVy4cGZ.css +1 -0
  7. package/dist/web/assets/{PluginManager-2RlvEJCT.js → PluginManager-BTa-Ygf6.js} +1 -1
  8. package/dist/web/assets/{ProjectList-_ecCngpl.js → ProjectList-F_cB8ldA.js} +1 -1
  9. package/dist/web/assets/{SessionList-DlWcw3hO.js → SessionList-LOrbPQvu.js} +1 -1
  10. package/dist/web/assets/SkillManager-BsX8Qpj0.css +1 -0
  11. package/dist/web/assets/SkillManager-BzI_t-yV.js +1 -0
  12. package/dist/web/assets/{WorkspaceManager-3mqghi09.js → WorkspaceManager-Db9RGiz7.js} +1 -1
  13. package/dist/web/assets/index-DIWJmyL6.js +2 -0
  14. package/dist/web/assets/{index-BceXgSSe.css → index-bJRG3RXH.css} +1 -1
  15. package/dist/web/index.html +2 -2
  16. package/docs/superpowers/plans/2026-08-01-omp-skill-scan-settings.md +759 -0
  17. package/docs/superpowers/specs/2026-08-01-omp-skill-scan-settings-design.md +224 -0
  18. package/package.json +1 -1
  19. package/src/commands/proxy-control.js +12 -12
  20. package/src/commands/toggle-proxy.js +9 -9
  21. package/src/config/paths.js +53 -29
  22. package/src/index.js +1 -1
  23. package/src/server/api/plugins.js +119 -47
  24. package/src/server/api/skills.js +63 -10
  25. package/src/server/omp-proxy-server.js +17 -4
  26. package/src/server/services/channel-health.js +5 -0
  27. package/src/server/services/channel-sync-utils.js +1 -1
  28. package/src/server/services/model-definition-schema.js +2 -1
  29. package/src/server/services/omp-channels.js +213 -26
  30. package/src/server/services/omp-config.js +71 -6
  31. package/src/server/services/omp-native-plugin-adapter.js +456 -0
  32. package/src/server/services/omp-settings-manager.js +3 -1
  33. package/src/server/services/omp-skill-discovery.js +358 -0
  34. package/src/server/services/omp-skill-settings-service.js +97 -0
  35. package/src/server/services/pi-omp-migration.js +273 -0
  36. package/src/server/services/platform-resolution.js +44 -0
  37. package/src/server/services/plugins-service.js +85 -561
  38. package/src/server/services/project-path-validation.js +65 -0
  39. package/src/server/services/skill-service.js +169 -77
  40. package/dist/web/assets/Home-Bft3Kztl.js +0 -1
  41. package/dist/web/assets/Home-CHZJkkjH.css +0 -1
  42. package/dist/web/assets/SkillManager-CnY7IXkX.js +0 -1
  43. package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
  44. 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.syncManagedOmpProvidersIfEnabled(allChannels);
217
+ this.syncOmpProvidersForCurrentMode(allChannels);
148
218
  }
149
219
 
150
220
  _onAfterUpdate(_oldChannel, _newChannel, allChannels) {
151
- this.syncManagedOmpProvidersIfEnabled(allChannels);
221
+ this.syncOmpProvidersForCurrentMode(allChannels);
152
222
  }
153
223
 
154
224
  _onAfterDelete(_channel, allChannels) {
155
- this.syncManagedOmpProvidersIfEnabled(allChannels);
225
+ this.syncOmpProvidersForCurrentMode(allChannels);
156
226
  }
157
227
 
158
- syncManagedOmpProvidersIfEnabled(channels = this.getChannels().channels) {
159
- if (!isManagedOmpModeEnabled()) {
160
- return getLastManagedOmpSyncResult();
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 state = loadManagedOmpModeState();
163
- return this.syncManagedOmpProviders(channels, state?.gateway ? {
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.syncManagedOmpProvidersIfEnabled(channels);
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 = modelsConfig?.providers?.[providerId] || null;
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 baseUrl = String(provider.baseUrl || provider.base_url || '').trim();
400
- const originalProviderId = getOriginalOmpProviderId(providerId);
401
- const existing = findOmpExistingChannel(channels, providerId, baseUrl);
402
- let credential = resolveApiKeyValue(provider.apiKey || provider.api_key || '');
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 apiKey = credential.value || existing?.apiKey || '';
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}" 缺少可解析 API Key,OAuth/登录态渠道不支持同步导入。`
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: 'api_key',
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(env.OMP_CODING_AGENT_DIR || '');
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,