coding-tool-x 3.8.9 → 3.9.0
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/README.md +59 -387
- package/dist/web/assets/AgentsDrawer-DooV5Qhx.js +1 -0
- package/dist/web/assets/Analytics-BXRwqC-P.js +1 -0
- package/dist/web/assets/AssetPathField-B-LHvOxq.js +1 -0
- package/dist/web/assets/{ChatHistoryDrawer-BTmQ1aQr.js → ChatHistoryDrawer-CoHlGAxb.js} +1 -1
- package/dist/web/assets/{CollapseItem-CwqJk3bE.js → CollapseItem-kJkiHxFl.js} +1 -1
- package/dist/web/assets/CommandsDrawer-DLQrikcw.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-BItwo0FQ.js +1 -0
- package/dist/web/assets/{ConfigTemplates-gyERg9iC.js → ConfigTemplates-R3YzzBT3.js} +1 -1
- package/dist/web/assets/{ConfigTemplatesDrawer-B17RqFi4.js → ConfigTemplatesDrawer-C_nPKrhx.js} +1 -1
- package/dist/web/assets/ConfigTemplatesPanel-BiwrNyYN.js +1 -0
- package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
- package/dist/web/assets/FavoritesDrawer-BAJ-AOpN.js +1 -0
- package/dist/web/assets/GatewayConvertDrawer-Av3yzwhP.js +1 -0
- package/dist/web/assets/Home-B01aL_SR.js +1 -0
- package/dist/web/assets/{MarkdownEditor-9TTwTS-U.js → MarkdownEditor-D75GGt5X.js} +1 -1
- package/dist/web/assets/McpDrawer-xNzJtT4X.js +1 -0
- package/dist/web/assets/McpFormDrawer-B240mJ5y.js +1 -0
- package/dist/web/assets/{PluginManager-BuRcWuBj.js → PluginManager-CgLjBdlr.js} +1 -1
- package/dist/web/assets/{PluginsDrawer-_zZENaQn.js → PluginsDrawer-DvJdwMXx.js} +1 -1
- package/dist/web/assets/PluginsPanel-kX14E_DT.js +1 -0
- package/dist/web/assets/ProjectList-CRtc3Hhx.js +1 -0
- package/dist/web/assets/PromptsDrawer-CvT9dA2N.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-DM1gf584.js +1 -0
- package/dist/web/assets/SessionCard-cZR2V4Br.js +1 -0
- package/dist/web/assets/SessionList-D7-P4Zme.js +1 -0
- package/dist/web/assets/SettingsDrawer-BFzaxbgC.css +1 -0
- package/dist/web/assets/SettingsDrawer-gW8kh-S8.js +1 -0
- package/dist/web/assets/{SkillManager-S698Hns8.js → SkillManager-D0BlI7CG.js} +1 -1
- package/dist/web/assets/{SkillsDrawer-Ms4lPCow.js → SkillsDrawer-BMB6uzqP.js} +1 -1
- package/dist/web/assets/SkillsPanel-BzR828De.js +1 -0
- package/dist/web/assets/Upload-3fNyOQrx.js +1 -0
- package/dist/web/assets/{WorkspaceDrawer-L7HJRc6u.js → WorkspaceDrawer-DvtBA58Q.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DsoNg2z8.js → WorkspaceManager-BW5KtsHO.js} +1 -1
- package/dist/web/assets/WorkspacePanel-BC_xpAEZ.js +1 -0
- package/dist/web/assets/client-BgcNg4Bt.js +1 -0
- package/dist/web/assets/{headers-B5JWGn3v.js → headers-DbowkmZJ.js} +1 -1
- package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
- package/dist/web/assets/index-BFqnddoI.js +2 -0
- package/dist/web/assets/index-Bn8BUnwP.css +1 -0
- package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
- package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
- package/dist/web/assets/platforms-mqqe_82d.js +1 -0
- package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
- package/dist/web/assets/sessions-53-Fr68H.js +1 -0
- package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
- package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
- package/dist/web/assets/{useResponsiveDrawer-V_onl9NV.js → useResponsiveDrawer-Ckgj760z.js} +1 -1
- package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
- package/dist/web/index.html +8 -8
- package/docs/analytics.png +0 -0
- package/docs/home.png +0 -0
- package/package.json +4 -2
- package/src/commands/export-config.js +17 -211
- package/src/config/model-metadata.json +180 -16
- package/src/platforms/channel-auth-service.js +1 -1
- package/src/platforms/commands-service.js +28 -0
- package/src/platforms/driver-registry.js +1 -0
- package/src/platforms/drivers/claude/channels-implementation.js +79 -90
- package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
- package/src/platforms/drivers/codex/channels-implementation.js +34 -49
- package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
- package/src/platforms/drivers/dsh/api-operations.js +1 -1
- package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
- package/src/platforms/drivers/dsh/channels.js +0 -18
- package/src/platforms/drivers/dsh/common.js +7 -0
- package/src/platforms/drivers/dsh/native-config.js +7 -2
- package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
- package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
- package/src/platforms/drivers/generic-native-config.js +20 -0
- package/src/platforms/drivers/native-config-snapshot.js +30 -4
- package/src/platforms/drivers/omp/channels-implementation.js +51 -17
- package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
- package/src/platforms/drivers/opencode/channels-implementation.js +6 -0
- package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
- package/src/platforms/manifest-schema.js +35 -2
- package/src/platforms/manifests/claude.json +0 -8
- package/src/platforms/manifests/codex.json +0 -8
- package/src/platforms/manifests/dsh.json +6 -9
- package/src/platforms/manifests/gemini.json +0 -8
- package/src/platforms/manifests/omp.json +0 -8
- package/src/platforms/manifests/opencode.json +0 -8
- package/src/platforms/native-oauth-adapters.js +33 -33
- package/src/platforms/remote-notification-providers.js +5 -5
- package/src/server/api/settings.js +6 -5
- package/src/server/index.js +2 -0
- package/src/server/services/bounded-content.js +60 -0
- package/src/server/services/channel-balance.js +10 -4
- package/src/server/services/config-export-service.js +2 -2
- package/src/server/services/config-templates-service.js +5 -1
- package/src/server/services/plugins-service.js +162 -85
- package/src/server/services/session-history-maintenance-worker.js +71 -0
- package/src/server/services/session-history-maintenance.js +166 -0
- package/src/server/services/skill-service.js +23 -3
- package/src/server/services/speed-test.js +0 -72
- package/src/shared/channel-driver.js +16 -0
- package/src/shared/driver-factories/api.js +1 -0
- package/src/utils/native-config-patcher.js +474 -0
- package/dist/web/assets/AgentsDrawer-Cj5CJ4d9.js +0 -1
- package/dist/web/assets/Analytics-1doS-8uC.js +0 -1
- package/dist/web/assets/AssetPathField-C9TOPl7o.js +0 -1
- package/dist/web/assets/CommandsDrawer-CaZWDI_G.js +0 -1
- package/dist/web/assets/ConfigExportDrawer-C3vSZEsS.js +0 -1
- package/dist/web/assets/ConfigTemplatesPanel-DKIC_FqG.js +0 -1
- package/dist/web/assets/FavoritesDrawer-DaziQmBp.js +0 -1
- package/dist/web/assets/GatewayConvertDrawer-OMgWUUSc.js +0 -1
- package/dist/web/assets/Home-DOSUTfrE.js +0 -1
- package/dist/web/assets/McpDrawer-I8rl8lGH.js +0 -1
- package/dist/web/assets/McpFormDrawer-DEP3C7ut.js +0 -1
- package/dist/web/assets/PluginsPanel-Bs5qZ-TK.js +0 -1
- package/dist/web/assets/ProjectList-BSxuL2td.js +0 -1
- package/dist/web/assets/PromptsDrawer-CoZ-ZtsY.js +0 -1
- package/dist/web/assets/RecentSessionsDrawer-B_17aJBG.js +0 -1
- package/dist/web/assets/SessionCard-BL_x0d3t.js +0 -1
- package/dist/web/assets/SessionList-DhRYZ6vl.js +0 -1
- package/dist/web/assets/SettingsDrawer-Dw5NUgHC.css +0 -1
- package/dist/web/assets/SettingsDrawer-fYb0DU7X.js +0 -1
- package/dist/web/assets/SkillsPanel-rqnqAOGL.js +0 -1
- package/dist/web/assets/SpeedTestDrawer-Bbn38SfV.js +0 -1
- package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
- package/dist/web/assets/Upload-CdyVHxCI.js +0 -1
- package/dist/web/assets/WorkspacePanel-Be8PVZah.js +0 -1
- package/dist/web/assets/client-UDwXBP9i.js +0 -1
- package/dist/web/assets/index-3Lp6z8TQ.css +0 -1
- package/dist/web/assets/index-BKlAWTH-.js +0 -2
- package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
- package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
- package/dist/web/assets/project-config-B2baPzYL.js +0 -1
- package/dist/web/assets/sessions-CC0j-KH_.js +0 -1
- package/dist/web/assets/use-message-DqCwefG6.js +0 -1
|
@@ -45,11 +45,14 @@ const { EffectiveControlService } = require('./effective-control-service');
|
|
|
45
45
|
const { SkillArtifactStore } = require('./skill-artifact-store');
|
|
46
46
|
const { SkillFormatAdapter } = require('./skill-format-adapters');
|
|
47
47
|
const { SkillProjectionService } = require('./skill-projection-service');
|
|
48
|
+
const { readTextFileLimited, truncateUtf8 } = require('./bounded-content');
|
|
48
49
|
|
|
49
50
|
const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
|
|
50
51
|
const DEFAULT_GITHUB_HOST = 'https://github.com';
|
|
51
52
|
const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
|
|
52
53
|
const CACHE_TTL = 5 * 60 * 1000;
|
|
54
|
+
const MAX_SKILL_DETAIL_BYTES = 128 * 1024;
|
|
55
|
+
const MAX_PREPARED_SKILL_CACHE_ENTRIES = 32;
|
|
53
56
|
|
|
54
57
|
function sanitizeRefreshError(value) {
|
|
55
58
|
return String(value || '')
|
|
@@ -392,7 +395,16 @@ class SkillService {
|
|
|
392
395
|
invocation: skill.invocation || null,
|
|
393
396
|
...(skill.whenToUse ? { whenToUse: skill.whenToUse } : {}),
|
|
394
397
|
...(skill.metadata ? { metadata: skill.metadata } : {}),
|
|
395
|
-
...(includeContent && skill.content !== undefined
|
|
398
|
+
...(includeContent && skill.content !== undefined
|
|
399
|
+
? (() => {
|
|
400
|
+
const bounded = truncateUtf8(skill.content, MAX_SKILL_DETAIL_BYTES);
|
|
401
|
+
return {
|
|
402
|
+
content: bounded.text,
|
|
403
|
+
contentBytes: bounded.bytes,
|
|
404
|
+
contentTruncated: bounded.truncated
|
|
405
|
+
};
|
|
406
|
+
})()
|
|
407
|
+
: {})
|
|
396
408
|
};
|
|
397
409
|
}
|
|
398
410
|
|
|
@@ -534,7 +546,11 @@ class SkillService {
|
|
|
534
546
|
return value.map(skill => ({ ...skill }));
|
|
535
547
|
}
|
|
536
548
|
const entry = { value, cachedAt: Date.now(), generation };
|
|
549
|
+
this._preparedSkillsCache.delete(cacheKey);
|
|
537
550
|
this._preparedSkillsCache.set(cacheKey, entry);
|
|
551
|
+
while (this._preparedSkillsCache.size > MAX_PREPARED_SKILL_CACHE_ENTRIES) {
|
|
552
|
+
this._preparedSkillsCache.delete(this._preparedSkillsCache.keys().next().value);
|
|
553
|
+
}
|
|
538
554
|
if (cacheKey === 'user:') {
|
|
539
555
|
this.skillsCache = value;
|
|
540
556
|
this.cacheTime = entry.cachedAt;
|
|
@@ -3663,7 +3679,8 @@ ${content}
|
|
|
3663
3679
|
if (!fs.existsSync(skillFile) || fs.lstatSync(skillFile).isSymbolicLink()) {
|
|
3664
3680
|
throw new Error('Skill detail contains an unavailable or symlinked SKILL.md');
|
|
3665
3681
|
}
|
|
3666
|
-
const
|
|
3682
|
+
const boundedContent = readTextFileLimited(skillFile, MAX_SKILL_DETAIL_BYTES);
|
|
3683
|
+
const content = boundedContent.text;
|
|
3667
3684
|
const parsed = this.parseSkillMd(content);
|
|
3668
3685
|
const bodyMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
|
|
3669
3686
|
const body = bodyMatch ? bodyMatch[1].trim() : content;
|
|
@@ -3676,6 +3693,8 @@ ${content}
|
|
|
3676
3693
|
description: parsed.description || metadata.description || '',
|
|
3677
3694
|
content: body,
|
|
3678
3695
|
fullContent: content,
|
|
3696
|
+
contentBytes: boundedContent.bytes,
|
|
3697
|
+
contentTruncated: boundedContent.truncated,
|
|
3679
3698
|
installed: enabled,
|
|
3680
3699
|
enabled,
|
|
3681
3700
|
cached: metadata.cached !== false,
|
|
@@ -3881,5 +3900,6 @@ ${content}
|
|
|
3881
3900
|
module.exports = {
|
|
3882
3901
|
SkillService,
|
|
3883
3902
|
DEFAULT_REPOS: DEFAULT_REPOS_BY_PLATFORM.claude,
|
|
3884
|
-
DEFAULT_REPOS_BY_PLATFORM
|
|
3903
|
+
DEFAULT_REPOS_BY_PLATFORM,
|
|
3904
|
+
MAX_SKILL_DETAIL_BYTES
|
|
3885
3905
|
};
|
|
@@ -38,47 +38,6 @@ function sanitizeTimeout(timeout) {
|
|
|
38
38
|
return Math.min(Math.max(ms, MIN_TIMEOUT), MAX_TIMEOUT);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* 规范化批量测速并发度(默认小并发)
|
|
43
|
-
*/
|
|
44
|
-
function sanitizeBatchConcurrency(concurrency, defaultValue = 2) {
|
|
45
|
-
const value = Number(concurrency);
|
|
46
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
47
|
-
return defaultValue;
|
|
48
|
-
}
|
|
49
|
-
return Math.min(Math.max(Math.round(value), 1), 5);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 按并发限制执行异步任务,保持结果顺序与输入一致
|
|
54
|
-
*/
|
|
55
|
-
async function runWithConcurrencyLimit(items, concurrency, taskFn) {
|
|
56
|
-
const list = Array.isArray(items) ? items : [];
|
|
57
|
-
if (list.length === 0) return [];
|
|
58
|
-
|
|
59
|
-
const limit = sanitizeBatchConcurrency(concurrency);
|
|
60
|
-
const results = new Array(list.length);
|
|
61
|
-
let cursor = 0;
|
|
62
|
-
|
|
63
|
-
async function worker() {
|
|
64
|
-
while (true) {
|
|
65
|
-
const currentIndex = cursor;
|
|
66
|
-
cursor += 1;
|
|
67
|
-
if (currentIndex >= list.length) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
results[currentIndex] = await taskFn(list[currentIndex], currentIndex);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const workers = [];
|
|
75
|
-
const workerCount = Math.min(limit, list.length);
|
|
76
|
-
for (let i = 0; i < workerCount; i += 1) {
|
|
77
|
-
workers.push(worker());
|
|
78
|
-
}
|
|
79
|
-
await Promise.all(workers);
|
|
80
|
-
return results;
|
|
81
|
-
}
|
|
82
41
|
|
|
83
42
|
function normalizeNonEmptyString(value) {
|
|
84
43
|
if (typeof value !== 'string') return null;
|
|
@@ -768,34 +727,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
768
727
|
return primaryResult;
|
|
769
728
|
}
|
|
770
729
|
|
|
771
|
-
/**
|
|
772
|
-
* 批量测试多个渠道
|
|
773
|
-
* @param {Array} channels - 渠道列表
|
|
774
|
-
* @param {number} timeout - 超时时间
|
|
775
|
-
* @param {string} channelType - 渠道类型:'claude' | 'codex' | 'gemini'
|
|
776
|
-
* @returns {Promise<Array>} 测试结果列表
|
|
777
|
-
*/
|
|
778
|
-
async function testMultipleChannels(channels, timeout = DEFAULT_TIMEOUT, channelType = 'claude', concurrency = 2) {
|
|
779
|
-
const results = await runWithConcurrencyLimit(
|
|
780
|
-
channels,
|
|
781
|
-
concurrency,
|
|
782
|
-
channel => testChannelSpeed(channel, timeout, channelType)
|
|
783
|
-
);
|
|
784
|
-
|
|
785
|
-
// 按延迟排序(成功的在前,按延迟升序)
|
|
786
|
-
results.sort((a, b) => {
|
|
787
|
-
if (a.success && !b.success) return -1;
|
|
788
|
-
if (!a.success && b.success) return 1;
|
|
789
|
-
if (a.success && b.success) {
|
|
790
|
-
const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
|
|
791
|
-
const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
|
|
792
|
-
return aLatency - bLatency;
|
|
793
|
-
}
|
|
794
|
-
return 0;
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
return results;
|
|
798
|
-
}
|
|
799
730
|
|
|
800
731
|
/**
|
|
801
732
|
* 获取缓存的测试结果
|
|
@@ -834,10 +765,7 @@ function getLatencyLevel(latency) {
|
|
|
834
765
|
|
|
835
766
|
module.exports = {
|
|
836
767
|
testChannelSpeed,
|
|
837
|
-
testMultipleChannels,
|
|
838
768
|
getCachedResult,
|
|
839
769
|
clearCache,
|
|
840
770
|
getLatencyLevel,
|
|
841
|
-
sanitizeBatchConcurrency,
|
|
842
|
-
runWithConcurrencyLimit
|
|
843
771
|
};
|
|
@@ -202,6 +202,22 @@ function createChannelDriver({
|
|
|
202
202
|
return failed(platform, capability, operation, error);
|
|
203
203
|
}
|
|
204
204
|
};
|
|
205
|
+
driver.speedTest = async (request = {}) => {
|
|
206
|
+
const operation = 'speedTest';
|
|
207
|
+
const channelId = request?.params?.channelId || request?.channelId;
|
|
208
|
+
const body = request?.body && typeof request.body === 'object' ? request.body : request;
|
|
209
|
+
if (!channelId) return invalid(platform, capability, operation, new Error('Channel id is required'));
|
|
210
|
+
try {
|
|
211
|
+
const channel = await findChannelById(channelId);
|
|
212
|
+
if (!channel) return invalid(platform, capability, operation, new Error(`Channel not found: ${channelId}`));
|
|
213
|
+
const { testChannelSpeed } = require('../server/services/speed-test');
|
|
214
|
+
const channelType = resolveModelListType(platform, channel, modelListType);
|
|
215
|
+
const data = await testChannelSpeed(channel, body?.timeout, channelType, { authSourceType: platform });
|
|
216
|
+
return ok(platform, capability, operation, data);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
return failed(platform, capability, operation, error);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
205
221
|
driver.list = (...args) => call('list', listMethod, args, sanitizeChannels);
|
|
206
222
|
driver.getEnabled = (...args) => call('getEnabled', 'getEnabledChannels', args, channels => sanitizeChannels(channels).channels);
|
|
207
223
|
driver.create = (input, ...rest) => {
|
|
@@ -71,6 +71,7 @@ function argsFor(capability, operation, context) {
|
|
|
71
71
|
if (operation === 'update') return [params.channelId, context.body || {}];
|
|
72
72
|
if (operation === 'remove') return [params.channelId];
|
|
73
73
|
if (operation === 'models') return [params.channelId, options];
|
|
74
|
+
if (['speedTest', 'getAuth', 'syncLocalAuth', 'getAuthQuota'].includes(operation)) return [context];
|
|
74
75
|
if (operation === 'applyToSettings') return [params.channelId];
|
|
75
76
|
return [context.body || options];
|
|
76
77
|
}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const jsonc = require('jsonc-parser');
|
|
7
|
+
const YAML = require('yaml');
|
|
8
|
+
const toml = require('toml');
|
|
9
|
+
const tomlStringify = require('@iarna/toml').stringify;
|
|
10
|
+
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function valuesEqual(left, right) {
|
|
16
|
+
if (left === right) return true;
|
|
17
|
+
if (left instanceof Date || right instanceof Date) {
|
|
18
|
+
if (left instanceof Date && right instanceof Date) return left.getTime() === right.getTime();
|
|
19
|
+
const date = left instanceof Date ? left : right;
|
|
20
|
+
const scalar = left instanceof Date ? right : left;
|
|
21
|
+
return typeof scalar === 'string'
|
|
22
|
+
&& Number.isFinite(Date.parse(scalar))
|
|
23
|
+
&& Date.parse(scalar) === date.getTime();
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
26
|
+
return Array.isArray(left) && Array.isArray(right)
|
|
27
|
+
&& left.length === right.length
|
|
28
|
+
&& left.every((value, index) => valuesEqual(value, right[index]));
|
|
29
|
+
}
|
|
30
|
+
if (isPlainObject(left) || isPlainObject(right)) {
|
|
31
|
+
if (!isPlainObject(left) || !isPlainObject(right)) return false;
|
|
32
|
+
const leftKeys = Object.keys(left);
|
|
33
|
+
const rightKeys = Object.keys(right);
|
|
34
|
+
return leftKeys.length === rightKeys.length
|
|
35
|
+
&& leftKeys.every(key => Object.prototype.hasOwnProperty.call(right, key)
|
|
36
|
+
&& valuesEqual(left[key], right[key]));
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cloneConfigValue(value) {
|
|
42
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
43
|
+
if (Array.isArray(value)) return value.map(cloneConfigValue);
|
|
44
|
+
if (isPlainObject(value)) {
|
|
45
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneConfigValue(entry)]));
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureParent(filePath) {
|
|
51
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeContent(filePath, content, options = {}) {
|
|
55
|
+
ensureParent(filePath);
|
|
56
|
+
if (!options.atomic) {
|
|
57
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
58
|
+
} else {
|
|
59
|
+
const mode = options.mode !== undefined
|
|
60
|
+
? options.mode
|
|
61
|
+
: (fs.existsSync(filePath) ? fs.statSync(filePath).mode & 0o777 : 0o600);
|
|
62
|
+
const temporary = `${filePath}.ctx-tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
63
|
+
try {
|
|
64
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', mode });
|
|
65
|
+
if (process.platform !== 'win32') fs.chmodSync(temporary, mode);
|
|
66
|
+
fs.renameSync(temporary, filePath);
|
|
67
|
+
} finally {
|
|
68
|
+
try { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); } catch (_) {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (options.mode !== undefined && process.platform !== 'win32') {
|
|
72
|
+
fs.chmodSync(filePath, options.mode);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseJsonc(content, filePath = 'config.json') {
|
|
77
|
+
const errors = [];
|
|
78
|
+
const value = jsonc.parse(content, errors, { allowTrailingComma: true, disallowComments: false });
|
|
79
|
+
if (errors.length > 0) {
|
|
80
|
+
const details = errors.map(error => jsonc.printParseErrorCode(error.error)).join(', ');
|
|
81
|
+
throw new Error(`Failed to parse ${path.basename(filePath)}: ${details}`);
|
|
82
|
+
}
|
|
83
|
+
if (!isPlainObject(value)) {
|
|
84
|
+
throw new Error(`${path.basename(filePath)} must contain a JSON object`);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reconcileJsonc(content, current, desired, filePath) {
|
|
90
|
+
let output = content;
|
|
91
|
+
const changes = [];
|
|
92
|
+
|
|
93
|
+
function visit(before, after, nodePath) {
|
|
94
|
+
if (valuesEqual(before, after)) return;
|
|
95
|
+
if (isPlainObject(before) && isPlainObject(after)) {
|
|
96
|
+
Object.keys(before).forEach((key) => {
|
|
97
|
+
if (!Object.prototype.hasOwnProperty.call(after, key)) {
|
|
98
|
+
changes.push({ path: [...nodePath, key], value: undefined });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
Object.keys(after).forEach((key) => {
|
|
102
|
+
if (!Object.prototype.hasOwnProperty.call(before, key)) {
|
|
103
|
+
changes.push({ path: [...nodePath, key], value: after[key] });
|
|
104
|
+
} else {
|
|
105
|
+
visit(before[key], after[key], [...nodePath, key]);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
changes.push({ path: nodePath, value: after });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
visit(current, desired, []);
|
|
114
|
+
for (const change of changes) {
|
|
115
|
+
const edits = jsonc.modify(output, change.path, change.value, {
|
|
116
|
+
formattingOptions: {
|
|
117
|
+
insertSpaces: true,
|
|
118
|
+
tabSize: 2,
|
|
119
|
+
eol: output.includes('\r\n') ? '\r\n' : '\n'
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
output = jsonc.applyEdits(output, edits);
|
|
123
|
+
}
|
|
124
|
+
return output;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function updateJsoncFile(filePath, mutate, options = {}) {
|
|
128
|
+
const exists = fs.existsSync(filePath);
|
|
129
|
+
const original = exists ? fs.readFileSync(filePath, 'utf8') : '{}\n';
|
|
130
|
+
const current = original.trim() ? parseJsonc(original, filePath) : {};
|
|
131
|
+
const desired = cloneConfigValue(current);
|
|
132
|
+
mutate(desired);
|
|
133
|
+
const output = reconcileJsonc(original, current, desired, filePath);
|
|
134
|
+
if (output !== original) {
|
|
135
|
+
writeContent(filePath, output, options);
|
|
136
|
+
}
|
|
137
|
+
return desired;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readJsoncFile(filePath, fallback = {}) {
|
|
141
|
+
if (!fs.existsSync(filePath)) return JSON.parse(JSON.stringify(fallback));
|
|
142
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
143
|
+
if (!original.trim()) return JSON.parse(JSON.stringify(fallback));
|
|
144
|
+
return parseJsonc(original, filePath);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeJsoncFile(filePath, value, options = {}) {
|
|
148
|
+
return updateJsoncFile(filePath, (current) => {
|
|
149
|
+
Object.keys(current).forEach(key => delete current[key]);
|
|
150
|
+
Object.assign(current, cloneConfigValue(value || {}));
|
|
151
|
+
}, options);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function reconcileYamlDocument(document, current, desired, nodePath = []) {
|
|
155
|
+
if (valuesEqual(current, desired)) return;
|
|
156
|
+
if (isPlainObject(current) && isPlainObject(desired)) {
|
|
157
|
+
Object.keys(current).forEach((key) => {
|
|
158
|
+
if (!Object.prototype.hasOwnProperty.call(desired, key)) {
|
|
159
|
+
document.deleteIn([...nodePath, key]);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
Object.keys(desired).forEach((key) => {
|
|
163
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)) {
|
|
164
|
+
document.setIn([...nodePath, key], desired[key]);
|
|
165
|
+
} else {
|
|
166
|
+
reconcileYamlDocument(document, current[key], desired[key], [...nodePath, key]);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
document.setIn(nodePath, desired);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function updateYamlFile(filePath, mutate, options = {}) {
|
|
175
|
+
const exists = fs.existsSync(filePath);
|
|
176
|
+
const original = exists ? fs.readFileSync(filePath, 'utf8') : '{}\n';
|
|
177
|
+
const document = YAML.parseDocument(original, {
|
|
178
|
+
keepSourceTokens: true,
|
|
179
|
+
prettyErrors: true,
|
|
180
|
+
customTags: options.customTags
|
|
181
|
+
});
|
|
182
|
+
if (document.errors.length > 0) {
|
|
183
|
+
throw new Error(`Failed to parse ${path.basename(filePath)}: ${document.errors[0].message}`);
|
|
184
|
+
}
|
|
185
|
+
let current = document.toJS({ mapAsMap: false });
|
|
186
|
+
if (current === undefined || current === null) current = {};
|
|
187
|
+
else if (!isPlainObject(current)) {
|
|
188
|
+
throw new Error(`${path.basename(filePath)} must contain a YAML mapping`);
|
|
189
|
+
}
|
|
190
|
+
const desired = cloneConfigValue(current);
|
|
191
|
+
mutate(desired);
|
|
192
|
+
if (valuesEqual(current, desired)) return desired;
|
|
193
|
+
reconcileYamlDocument(document, current, desired);
|
|
194
|
+
const output = document.toString({ lineWidth: 120 });
|
|
195
|
+
writeContent(filePath, output, options);
|
|
196
|
+
return desired;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function writeYamlFile(filePath, value, options = {}) {
|
|
200
|
+
return updateYamlFile(filePath, (current) => {
|
|
201
|
+
Object.keys(current).forEach(key => delete current[key]);
|
|
202
|
+
Object.assign(current, cloneConfigValue(value || {}));
|
|
203
|
+
}, options);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function findTomlCommentStart(line) {
|
|
207
|
+
let quote = '';
|
|
208
|
+
let escaped = false;
|
|
209
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
210
|
+
const char = line[index];
|
|
211
|
+
if (escaped) { escaped = false; continue; }
|
|
212
|
+
if (char === '\\' && quote === '"') { escaped = true; continue; }
|
|
213
|
+
if ((char === '"' || char === "'") && (!quote || quote === char)) {
|
|
214
|
+
quote = quote ? '' : char;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (char === '#' && !quote) return index;
|
|
218
|
+
}
|
|
219
|
+
return -1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stripTomlComment(line) {
|
|
223
|
+
const commentIndex = findTomlCommentStart(line);
|
|
224
|
+
return commentIndex < 0 ? line : line.slice(0, commentIndex);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function tomlHeaderProviderKey(line) {
|
|
228
|
+
const header = stripTomlComment(line).trim();
|
|
229
|
+
const match = header.match(/^\[\s*model_providers\s*\.\s*(?:"((?:\\.|[^"\\])*)"|'([^']*)'|([A-Za-z0-9_-]+))\s*\]$/);
|
|
230
|
+
if (!match) return null;
|
|
231
|
+
if (match[1] !== undefined) {
|
|
232
|
+
try { return JSON.parse(`"${match[1]}"`); } catch (_) { return null; }
|
|
233
|
+
}
|
|
234
|
+
return match[2] !== undefined ? match[2] : match[3];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isNestedTomlProviderSection(line, providerKey) {
|
|
238
|
+
const escaped = String(providerKey).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
239
|
+
return new RegExp(`^\\s*\\[\\s*model_providers\\s*\\.\\s*(?:"${escaped}"|'${escaped}'|${escaped})\\s*\\.`)
|
|
240
|
+
.test(stripTomlComment(line).trimEnd());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function preserveTomlSectionPreamble(lines, sectionStart, nextSectionStart) {
|
|
244
|
+
let boundary = nextSectionStart;
|
|
245
|
+
while (boundary > sectionStart + 1) {
|
|
246
|
+
const line = lines[boundary - 1].trim();
|
|
247
|
+
if (line === '' || line.startsWith('#')) boundary -= 1;
|
|
248
|
+
else break;
|
|
249
|
+
}
|
|
250
|
+
return boundary;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function patchTomlProvider(content, providerKey, providerValue, eol) {
|
|
254
|
+
const lines = content.split(/(?<=\n)/);
|
|
255
|
+
const starts = [];
|
|
256
|
+
lines.forEach((line, index) => {
|
|
257
|
+
const parsedKey = tomlHeaderProviderKey(line.replace(/\r?\n$/, ''));
|
|
258
|
+
if (parsedKey === providerKey) starts.push(index);
|
|
259
|
+
});
|
|
260
|
+
const start = starts[0];
|
|
261
|
+
|
|
262
|
+
if (providerValue === undefined) {
|
|
263
|
+
if (start === undefined) return content;
|
|
264
|
+
let end = start + 1;
|
|
265
|
+
while (end < lines.length) {
|
|
266
|
+
if (!/^\s*\[(?!\[).+\]\s*(?:#.*)?(?:\r?\n)?$/.test(lines[end])) {
|
|
267
|
+
end += 1;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (isNestedTomlProviderSection(lines[end], providerKey)) {
|
|
271
|
+
end += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
end = preserveTomlSectionPreamble(lines, start, end);
|
|
277
|
+
lines.splice(start, end - start);
|
|
278
|
+
return lines.join('');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const generated = tomlStringify({ model_providers: { [providerKey]: providerValue } });
|
|
282
|
+
const generatedLines = generated.split(/(?<=\n)/);
|
|
283
|
+
const sectionStart = generatedLines.findIndex(line => tomlHeaderProviderKey(line.replace(/\r?\n$/, '')) === providerKey);
|
|
284
|
+
if (sectionStart < 0) throw new Error(`Failed to serialize Codex provider ${providerKey}`);
|
|
285
|
+
const section = generatedLines.slice(sectionStart).join('').trimEnd();
|
|
286
|
+
|
|
287
|
+
if (start !== undefined) {
|
|
288
|
+
let end = start + 1;
|
|
289
|
+
while (end < lines.length) {
|
|
290
|
+
if (!/^\s*\[(?!\[).+\]\s*(?:#.*)?(?:\r?\n)?$/.test(lines[end])) {
|
|
291
|
+
end += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (isNestedTomlProviderSection(lines[end], providerKey)) {
|
|
295
|
+
end += 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
const replaceEnd = preserveTomlSectionPreamble(lines, start, end);
|
|
301
|
+
const preservedComments = lines.slice(start, replaceEnd)
|
|
302
|
+
.map((line) => {
|
|
303
|
+
const withoutNewline = line.replace(/\r?\n$/, '');
|
|
304
|
+
const commentIndex = findTomlCommentStart(withoutNewline);
|
|
305
|
+
return commentIndex < 0 ? '' : withoutNewline.slice(commentIndex).trim();
|
|
306
|
+
})
|
|
307
|
+
.filter(Boolean);
|
|
308
|
+
const commentText = preservedComments.length > 0
|
|
309
|
+
? `${eol}${preservedComments.join(eol)}`
|
|
310
|
+
: '';
|
|
311
|
+
lines.splice(start, replaceEnd - start, `${section}${commentText}${eol}`);
|
|
312
|
+
return lines.join('');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let output = content;
|
|
316
|
+
if (output && !output.endsWith('\n')) output += eol;
|
|
317
|
+
if (output && !output.endsWith(`${eol}${eol}`)) output += eol;
|
|
318
|
+
return `${output}${section}${eol}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function writeTomlFile(filePath, desired, options = {}) {
|
|
322
|
+
const exists = fs.existsSync(filePath);
|
|
323
|
+
const original = exists ? fs.readFileSync(filePath, 'utf8') : '';
|
|
324
|
+
let current = {};
|
|
325
|
+
if (original.trim()) {
|
|
326
|
+
try {
|
|
327
|
+
current = toml.parse(original);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw new Error(`Failed to parse ${path.basename(filePath)}: ${error.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const before = current && typeof current === 'object' ? current : {};
|
|
333
|
+
const after = desired && typeof desired === 'object' ? desired : {};
|
|
334
|
+
const allowed = new Set(['model_provider', 'model_providers']);
|
|
335
|
+
const otherKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
336
|
+
otherKeys.forEach(key => {
|
|
337
|
+
if (!allowed.has(key) && !valuesEqual(before[key], after[key])) {
|
|
338
|
+
throw new Error(`Refusing to rewrite unrelated TOML field "${key}" in ${path.basename(filePath)}`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const eol = original.includes('\r\n') ? '\r\n' : '\n';
|
|
343
|
+
let output = original;
|
|
344
|
+
const hasCurrentProvider = Object.prototype.hasOwnProperty.call(before, 'model_provider');
|
|
345
|
+
const hasDesiredProvider = Object.prototype.hasOwnProperty.call(after, 'model_provider');
|
|
346
|
+
const providerChanged = hasCurrentProvider !== hasDesiredProvider
|
|
347
|
+
|| (hasCurrentProvider && before.model_provider !== after.model_provider);
|
|
348
|
+
if (providerChanged) {
|
|
349
|
+
const rootLines = output.split(/(?<=\n)/);
|
|
350
|
+
const headerIndex = rootLines.findIndex(line => /^\s*\[(?!\[).+\]/.test(line));
|
|
351
|
+
const rootEnd = headerIndex < 0 ? rootLines.length : headerIndex;
|
|
352
|
+
const settingIndex = rootLines.slice(0, rootEnd).findIndex(line => /^\s*model_provider\s*=/.test(line));
|
|
353
|
+
if (hasDesiredProvider) {
|
|
354
|
+
const serialized = tomlStringify({ model_provider: after.model_provider }).trimEnd();
|
|
355
|
+
if (settingIndex >= 0) {
|
|
356
|
+
const line = rootLines[settingIndex];
|
|
357
|
+
const newline = line.match(/\r?\n$/)?.[0] || '';
|
|
358
|
+
const assignment = line.replace(/\r?\n$/, '');
|
|
359
|
+
let commentIndex = -1;
|
|
360
|
+
let quoted = false;
|
|
361
|
+
let escaped = false;
|
|
362
|
+
for (let index = 0; index < assignment.length; index += 1) {
|
|
363
|
+
const char = assignment[index];
|
|
364
|
+
if (escaped) { escaped = false; continue; }
|
|
365
|
+
if (char === '\\' && quoted) { escaped = true; continue; }
|
|
366
|
+
if (char === '"') { quoted = !quoted; continue; }
|
|
367
|
+
if (char === '#' && !quoted) { commentIndex = index; break; }
|
|
368
|
+
}
|
|
369
|
+
const suffix = commentIndex >= 0 ? ` ${assignment.slice(commentIndex)}` : '';
|
|
370
|
+
const prefix = assignment.slice(0, assignment.indexOf('=') + 1);
|
|
371
|
+
rootLines[settingIndex] = `${prefix} ${serialized.slice(serialized.indexOf('=') + 1).trim()}${suffix}${newline}`;
|
|
372
|
+
} else {
|
|
373
|
+
const insertion = `${serialized}${eol}`;
|
|
374
|
+
rootLines.splice(rootEnd, 0, `${insertion}${rootEnd < rootLines.length ? eol : ''}`);
|
|
375
|
+
}
|
|
376
|
+
} else if (settingIndex >= 0) {
|
|
377
|
+
const line = rootLines[settingIndex];
|
|
378
|
+
const commentIndex = line.indexOf('#');
|
|
379
|
+
if (commentIndex >= 0) rootLines[settingIndex] = `${line.slice(commentIndex).trimStart()}`;
|
|
380
|
+
else rootLines.splice(settingIndex, 1);
|
|
381
|
+
}
|
|
382
|
+
output = rootLines.join('');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const currentProviders = isPlainObject(before.model_providers) ? before.model_providers : {};
|
|
386
|
+
const desiredProviders = isPlainObject(after.model_providers) ? after.model_providers : {};
|
|
387
|
+
const providerKeys = new Set([...Object.keys(currentProviders), ...Object.keys(desiredProviders)]);
|
|
388
|
+
for (const key of providerKeys) {
|
|
389
|
+
if (valuesEqual(currentProviders[key], desiredProviders[key])) continue;
|
|
390
|
+
output = patchTomlProvider(output, key, Object.prototype.hasOwnProperty.call(desiredProviders, key)
|
|
391
|
+
? desiredProviders[key]
|
|
392
|
+
: undefined, eol);
|
|
393
|
+
}
|
|
394
|
+
if (output !== original) writeContent(filePath, output, options);
|
|
395
|
+
return after;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function encodeEnvValue(value) {
|
|
399
|
+
const text = String(value ?? '');
|
|
400
|
+
if (!/[\s#'"\\]/.test(text) && !text.startsWith('=') && !text.endsWith('\r')) return text;
|
|
401
|
+
return JSON.stringify(text);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function findEnvComment(value) {
|
|
405
|
+
let quote = '';
|
|
406
|
+
let escaped = false;
|
|
407
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
408
|
+
const char = value[index];
|
|
409
|
+
if (escaped) { escaped = false; continue; }
|
|
410
|
+
if (char === '\\' && quote === '"') { escaped = true; continue; }
|
|
411
|
+
if ((char === '"' || char === "'") && (!quote || quote === char)) {
|
|
412
|
+
quote = quote ? '' : char;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (char === '#' && !quote && index > 0 && /\s/.test(value[index - 1])) {
|
|
416
|
+
let start = index;
|
|
417
|
+
while (start > 0 && /\s/.test(value[start - 1])) start -= 1;
|
|
418
|
+
return value.slice(start);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return '';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function updateEnvFile(filePath, updates, options = {}) {
|
|
425
|
+
const exists = fs.existsSync(filePath);
|
|
426
|
+
const original = exists ? fs.readFileSync(filePath, 'utf8') : '';
|
|
427
|
+
const eol = original.includes('\r\n') ? '\r\n' : '\n';
|
|
428
|
+
const lines = original.split(/\r?\n/);
|
|
429
|
+
const keys = new Set(Object.keys(updates || {}));
|
|
430
|
+
const seen = new Set();
|
|
431
|
+
const output = [];
|
|
432
|
+
|
|
433
|
+
for (const line of lines) {
|
|
434
|
+
const match = line.match(/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)\s*=([^\r\n]*)$/);
|
|
435
|
+
if (!match || !keys.has(match[2])) {
|
|
436
|
+
output.push(line);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const [, prefix, key] = match;
|
|
440
|
+
const value = updates[key];
|
|
441
|
+
const rawValue = match[3];
|
|
442
|
+
const comment = findEnvComment(rawValue);
|
|
443
|
+
if (value === undefined || value === null) {
|
|
444
|
+
if (comment) output.push(comment.trimStart());
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
output.push(`${prefix}${key}=${encodeEnvValue(value)}${comment ? ` ${comment.trimStart()}` : ''}`);
|
|
448
|
+
seen.add(key);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const missing = [...keys].filter(key => updates[key] !== undefined && updates[key] !== null && !seen.has(key));
|
|
452
|
+
if (missing.length > 0) {
|
|
453
|
+
while (output.length > 0 && output[output.length - 1] === '') output.pop();
|
|
454
|
+
output.push(...missing.map(key => `${key}=${encodeEnvValue(updates[key])}`));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let content = output.join(eol);
|
|
458
|
+
if (content && !content.endsWith(eol) && (original.endsWith('\n') || missing.length > 0)) content += eol;
|
|
459
|
+
if (content !== original) {
|
|
460
|
+
writeContent(filePath, content, options);
|
|
461
|
+
}
|
|
462
|
+
return content;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
parseJsonc,
|
|
467
|
+
readJsoncFile,
|
|
468
|
+
writeJsoncFile,
|
|
469
|
+
updateJsoncFile,
|
|
470
|
+
updateYamlFile,
|
|
471
|
+
writeYamlFile,
|
|
472
|
+
writeTomlFile,
|
|
473
|
+
updateEnvFile
|
|
474
|
+
};
|