aicodeswitch 3.9.3 → 4.0.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.
@@ -0,0 +1,198 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const toml = require('@iarna/toml');
4
+
5
+ /**
6
+ * TOML 解析器
7
+ */
8
+ const parseToml = (content) => {
9
+ try {
10
+ return toml.parse(content);
11
+ } catch (error) {
12
+ throw new Error(`Failed to parse TOML: ${error.message}`);
13
+ }
14
+ };
15
+
16
+ /**
17
+ * TOML 序列化器
18
+ */
19
+ const stringifyToml = (obj) => {
20
+ try {
21
+ return toml.stringify(obj);
22
+ } catch (error) {
23
+ throw new Error(`Failed to stringify TOML: ${error.message}`);
24
+ }
25
+ };
26
+
27
+ /**
28
+ * 深拷贝
29
+ */
30
+ const deepClone = (value) => JSON.parse(JSON.stringify(value));
31
+
32
+ /**
33
+ * 判断字段路径是否被管理(支持前缀匹配)
34
+ */
35
+ const isManagedPath = (fieldPath, managedFields) => {
36
+ return managedFields.some((managedField) => {
37
+ const managedPath = managedField.split('.');
38
+ if (fieldPath.length < managedPath.length) {
39
+ return false;
40
+ }
41
+
42
+ for (let i = 0; i < managedPath.length; i += 1) {
43
+ if (String(fieldPath[i]) !== managedPath[i]) {
44
+ return false;
45
+ }
46
+ }
47
+ return true;
48
+ });
49
+ };
50
+
51
+ /**
52
+ * 深度获取对象值
53
+ */
54
+ const deepGet = (obj, fieldPath) => {
55
+ let current = obj;
56
+ for (const segment of fieldPath) {
57
+ if (current === undefined || current === null) {
58
+ return undefined;
59
+ }
60
+ current = current[segment];
61
+ }
62
+ return current;
63
+ };
64
+
65
+ /**
66
+ * 深度设置对象值
67
+ */
68
+ const deepSet = (obj, fieldPath, value) => {
69
+ let current = obj;
70
+ for (let i = 0; i < fieldPath.length - 1; i += 1) {
71
+ const key = fieldPath[i];
72
+ if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
73
+ current[key] = {};
74
+ }
75
+ current = current[key];
76
+ }
77
+ current[fieldPath[fieldPath.length - 1]] = value;
78
+ };
79
+
80
+ /**
81
+ * 收集所有叶子字段路径(数组视为叶子)
82
+ */
83
+ const collectPaths = (obj, currentPath = [], allPaths = []) => {
84
+ if (obj === null || obj === undefined) {
85
+ if (currentPath.length > 0) {
86
+ allPaths.push(currentPath);
87
+ }
88
+ return allPaths;
89
+ }
90
+
91
+ if (Array.isArray(obj)) {
92
+ if (currentPath.length > 0) {
93
+ allPaths.push(currentPath);
94
+ }
95
+ return allPaths;
96
+ }
97
+
98
+ if (typeof obj !== 'object') {
99
+ if (currentPath.length > 0) {
100
+ allPaths.push(currentPath);
101
+ }
102
+ return allPaths;
103
+ }
104
+
105
+ const keys = Object.keys(obj);
106
+ if (keys.length === 0) {
107
+ if (currentPath.length > 0) {
108
+ allPaths.push(currentPath);
109
+ }
110
+ return allPaths;
111
+ }
112
+
113
+ for (const key of keys) {
114
+ collectPaths(obj[key], [...currentPath, key], allPaths);
115
+ }
116
+
117
+ return allPaths;
118
+ };
119
+
120
+ /**
121
+ * JSON 合并函数
122
+ * 以 source 为基础,合并 other 中的非管理字段
123
+ */
124
+ const mergeJsonSettings = (source, other, managedFields) => {
125
+ const result = deepClone(source);
126
+ const allPaths = collectPaths(other);
127
+
128
+ for (const fieldPath of allPaths) {
129
+ if (isManagedPath(fieldPath, managedFields)) {
130
+ continue;
131
+ }
132
+
133
+ const value = deepGet(other, fieldPath);
134
+ if (value !== undefined) {
135
+ deepSet(result, fieldPath, deepClone(value));
136
+ }
137
+ }
138
+
139
+ return result;
140
+ };
141
+
142
+ /**
143
+ * TOML 合并函数
144
+ * 以 source 为基础,合并 other 中的非管理字段
145
+ */
146
+ const mergeTomlSettings = (source, other, managedFields) => {
147
+ const result = deepClone(source);
148
+ const allPaths = collectPaths(other);
149
+
150
+ for (const fieldPath of allPaths) {
151
+ if (isManagedPath(fieldPath, managedFields)) {
152
+ continue;
153
+ }
154
+
155
+ const value = deepGet(other, fieldPath);
156
+ if (value !== undefined) {
157
+ deepSet(result, fieldPath, deepClone(value));
158
+ }
159
+ }
160
+
161
+ return result;
162
+ };
163
+
164
+ /**
165
+ * 原子性写入函数
166
+ * 先写入临时文件,然后原子性重命名
167
+ */
168
+ const atomicWriteFile = (filePath, content) => {
169
+ const tempFile = path.join(path.dirname(filePath), `.tmp_${path.basename(filePath)}`);
170
+
171
+ try {
172
+ // 确保目录存在
173
+ const dir = path.dirname(filePath);
174
+ if (!fs.existsSync(dir)) {
175
+ fs.mkdirSync(dir, { recursive: true });
176
+ }
177
+
178
+ // 写入临时文件
179
+ fs.writeFileSync(tempFile, content, 'utf-8');
180
+
181
+ // 原子性重命名
182
+ fs.renameSync(tempFile, filePath);
183
+ } catch (error) {
184
+ // 清理临时文件
185
+ if (fs.existsSync(tempFile)) {
186
+ fs.unlinkSync(tempFile);
187
+ }
188
+ throw error;
189
+ }
190
+ };
191
+
192
+ module.exports = {
193
+ parseToml,
194
+ stringifyToml,
195
+ mergeJsonSettings,
196
+ mergeTomlSettings,
197
+ atomicWriteFile,
198
+ };
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getManagedFields = exports.CODEX_AUTH_MANAGED_FIELDS = exports.CODEX_CONFIG_MANAGED_FIELDS = exports.CLAUDE_JSON_MANAGED_FIELDS = exports.CLAUDE_SETTINGS_MANAGED_FIELDS = void 0;
4
+ /**
5
+ * Claude Code settings.json 管理字段定义
6
+ */
7
+ exports.CLAUDE_SETTINGS_MANAGED_FIELDS = [
8
+ { path: ['env', 'ANTHROPIC_AUTH_TOKEN'] },
9
+ { path: ['env', 'ANTHROPIC_BASE_URL'] },
10
+ { path: ['env', 'API_TIMEOUT_MS'] },
11
+ { path: ['env', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'] },
12
+ { path: ['env', 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'], optional: true },
13
+ { path: ['env', 'ANTHROPIC_DEFAULT_HAIKU_MODEL'], optional: true },
14
+ { path: ['env', 'ANTHROPIC_DEFAULT_SONNET_MODEL'], optional: true },
15
+ { path: ['env', 'ANTHROPIC_DEFAULT_OPUS_MODEL'], optional: true },
16
+ { path: ['permissions'], optional: true },
17
+ { path: ['skipDangerousModePermissionPrompt'], optional: true },
18
+ { path: ['effortLevel'], optional: true },
19
+ { path: ['model'], optional: true },
20
+ ];
21
+ /**
22
+ * Claude Code .claude.json 管理字段定义
23
+ */
24
+ exports.CLAUDE_JSON_MANAGED_FIELDS = [
25
+ { path: ['hasCompletedOnboarding'] },
26
+ { path: ['mcpServers'], optional: true },
27
+ ];
28
+ /**
29
+ * Codex config.toml 管理字段定义
30
+ */
31
+ exports.CODEX_CONFIG_MANAGED_FIELDS = [
32
+ { path: ['model_provider'] },
33
+ { path: ['model'] },
34
+ { path: ['model_reasoning_effort'] },
35
+ { path: ['disable_response_storage'] },
36
+ { path: ['preferred_auth_method'] },
37
+ { path: ['requires_openai_auth'] },
38
+ { path: ['enableRouteSelection'] },
39
+ { path: ['model_providers', 'aicodeswitch'], isSection: true },
40
+ ];
41
+ /**
42
+ * Codex auth.json 管理字段定义
43
+ */
44
+ exports.CODEX_AUTH_MANAGED_FIELDS = [
45
+ { path: ['OPENAI_API_KEY'] },
46
+ ];
47
+ /**
48
+ * 根据配置类型和文件路径获取管理字段列表
49
+ */
50
+ const getManagedFields = (configType, filePath) => {
51
+ if (configType === 'claude') {
52
+ if (filePath.endsWith('settings.json')) {
53
+ return exports.CLAUDE_SETTINGS_MANAGED_FIELDS;
54
+ }
55
+ else if (filePath.endsWith('.claude.json')) {
56
+ return exports.CLAUDE_JSON_MANAGED_FIELDS;
57
+ }
58
+ }
59
+ else if (configType === 'codex') {
60
+ if (filePath.endsWith('config.toml')) {
61
+ return exports.CODEX_CONFIG_MANAGED_FIELDS;
62
+ }
63
+ else if (filePath.endsWith('auth.json')) {
64
+ return exports.CODEX_AUTH_MANAGED_FIELDS;
65
+ }
66
+ }
67
+ return [];
68
+ };
69
+ exports.getManagedFields = getManagedFields;
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.safeWriteConfig = exports.atomicWriteFile = exports.mergeTomlConfig = exports.stringifyToml = exports.parseToml = exports.mergeJsonConfig = exports.isFieldManaged = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const toml_1 = __importDefault(require("@iarna/toml"));
10
+ /**
11
+ * 判断字段路径是否匹配
12
+ * @param fieldPath 要检查的字段路径
13
+ * @param managedPath 管理字段路径定义
14
+ * @returns 是否匹配
15
+ */
16
+ const pathMatches = (fieldPath, managedPath) => {
17
+ if (fieldPath.length < managedPath.length) {
18
+ return false;
19
+ }
20
+ for (let i = 0; i < managedPath.length; i++) {
21
+ const fieldKey = fieldPath[i];
22
+ const managedKey = managedPath[i];
23
+ // 如果管理字段是通配符(数字索引),则匹配任何数字
24
+ if (typeof managedKey === 'number') {
25
+ if (typeof fieldKey !== 'number') {
26
+ return false;
27
+ }
28
+ }
29
+ else if (fieldKey !== managedKey) {
30
+ return false;
31
+ }
32
+ }
33
+ return true;
34
+ };
35
+ /**
36
+ * 判断字段是否被管理
37
+ * @param fieldPath 字段路径
38
+ * @param managedFields 管理字段列表
39
+ * @returns 是否被管理
40
+ */
41
+ const isFieldManaged = (fieldPath, managedFields) => {
42
+ for (const managed of managedFields) {
43
+ if (pathMatches(fieldPath, managed.path)) {
44
+ return true;
45
+ }
46
+ }
47
+ return false;
48
+ };
49
+ exports.isFieldManaged = isFieldManaged;
50
+ /**
51
+ * 递归遍历对象,收集叶子字段路径
52
+ * @param obj 要遍历的对象
53
+ * @param currentPath 当前路径
54
+ * @param allPaths 收集所有路径的数组
55
+ */
56
+ const collectPaths = (obj, currentPath, allPaths) => {
57
+ // 基础类型/null:当前路径即叶子
58
+ if (obj === null || obj === undefined || typeof obj !== 'object') {
59
+ if (currentPath.length > 0) {
60
+ allPaths.push(currentPath);
61
+ }
62
+ return;
63
+ }
64
+ // 数组:递归收集元素叶子;空数组本身视为叶子
65
+ if (Array.isArray(obj)) {
66
+ if (obj.length === 0) {
67
+ if (currentPath.length > 0) {
68
+ allPaths.push(currentPath);
69
+ }
70
+ return;
71
+ }
72
+ obj.forEach((item, index) => {
73
+ collectPaths(item, [...currentPath, index], allPaths);
74
+ });
75
+ return;
76
+ }
77
+ // 对象:递归收集子字段;空对象本身视为叶子
78
+ const keys = Object.keys(obj);
79
+ if (keys.length === 0) {
80
+ if (currentPath.length > 0) {
81
+ allPaths.push(currentPath);
82
+ }
83
+ return;
84
+ }
85
+ for (const key of keys) {
86
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
87
+ collectPaths(obj[key], [...currentPath, key], allPaths);
88
+ }
89
+ }
90
+ };
91
+ /**
92
+ * 深度获取对象的值
93
+ * @param obj 目标对象
94
+ * @param path 字段路径
95
+ * @returns 字段值
96
+ */
97
+ const deepGet = (obj, path) => {
98
+ let current = obj;
99
+ for (const key of path) {
100
+ if (current === null || current === undefined) {
101
+ return undefined;
102
+ }
103
+ current = current[key];
104
+ }
105
+ return current;
106
+ };
107
+ /**
108
+ * 深度设置对象的值
109
+ * @param obj 目标对象
110
+ * @param path 字段路径
111
+ * @param value 要设置的值
112
+ */
113
+ const deepSet = (obj, path, value) => {
114
+ let current = obj;
115
+ for (let i = 0; i < path.length - 1; i++) {
116
+ const key = path[i];
117
+ if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
118
+ current[key] = {};
119
+ }
120
+ current = current[key];
121
+ }
122
+ current[path[path.length - 1]] = value;
123
+ };
124
+ /**
125
+ * 合并 JSON 配置
126
+ * @param source 源配置(包含管理字段)
127
+ * @param other 其他配置(包含非管理字段)
128
+ * @param managedFields 管理字段列表
129
+ * @returns 合并后的配置
130
+ */
131
+ const mergeJsonConfig = (source, other, managedFields) => {
132
+ // 复制源配置作为基础
133
+ const result = JSON.parse(JSON.stringify(source));
134
+ // 收集 other 中所有的字段路径
135
+ const allPaths = [];
136
+ collectPaths(other, [], allPaths);
137
+ // 对于每个路径,如果不是管理字段,则从 other 中复制到 result
138
+ for (const fieldPath of allPaths) {
139
+ if (!(0, exports.isFieldManaged)(fieldPath, managedFields)) {
140
+ const value = deepGet(other, fieldPath);
141
+ deepSet(result, fieldPath, value);
142
+ }
143
+ }
144
+ return result;
145
+ };
146
+ exports.mergeJsonConfig = mergeJsonConfig;
147
+ /**
148
+ * 解析 TOML 配置文件
149
+ * @param content TOML 内容
150
+ * @returns 解析后的对象
151
+ */
152
+ const parseToml = (content) => {
153
+ try {
154
+ return toml_1.default.parse(content);
155
+ }
156
+ catch (error) {
157
+ throw new Error(`Failed to parse TOML: ${error}`);
158
+ }
159
+ };
160
+ exports.parseToml = parseToml;
161
+ /**
162
+ * 序列化对象为 TOML 格式
163
+ * @param obj 要序列化的对象
164
+ * @returns TOML 字符串
165
+ */
166
+ const stringifyToml = (obj) => {
167
+ // 使用 @iarna/toml 库的 stringify 方法
168
+ try {
169
+ return toml_1.default.stringify(obj);
170
+ }
171
+ catch (error) {
172
+ throw new Error(`Failed to stringify TOML: ${error}`);
173
+ }
174
+ };
175
+ exports.stringifyToml = stringifyToml;
176
+ /**
177
+ * 合并 TOML 配置
178
+ * @param source 源配置(包含管理字段)
179
+ * @param other 其他配置(包含非管理字段)
180
+ * @param managedFields 管理字段列表
181
+ * @returns 合并后的配置
182
+ */
183
+ const mergeTomlConfig = (source, other, managedFields) => {
184
+ // 复制源配置作为基础
185
+ const result = JSON.parse(JSON.stringify(source));
186
+ // 收集 other 中所有的字段路径
187
+ const allPaths = [];
188
+ collectPaths(other, [], allPaths);
189
+ // 对于每个路径,如果不是管理字段,则从 other 中复制到 result
190
+ for (const fieldPath of allPaths) {
191
+ if (!(0, exports.isFieldManaged)(fieldPath, managedFields)) {
192
+ const value = deepGet(other, fieldPath);
193
+ deepSet(result, fieldPath, value);
194
+ }
195
+ }
196
+ return result;
197
+ };
198
+ exports.mergeTomlConfig = mergeTomlConfig;
199
+ /**
200
+ * 原子性写入文件
201
+ * 先写入临时文件,然后重命名,确保写入失败时不会损坏原文件
202
+ * @param filePath 目标文件路径
203
+ * @param content 要写入的内容
204
+ */
205
+ const atomicWriteFile = (filePath, content) => {
206
+ const dir = path_1.default.dirname(filePath);
207
+ const tempFile = path_1.default.join(dir, `.tmp_${path_1.default.basename(filePath)}`);
208
+ try {
209
+ // 确保目录存在
210
+ if (!fs_1.default.existsSync(dir)) {
211
+ fs_1.default.mkdirSync(dir, { recursive: true });
212
+ }
213
+ // 写入临时文件
214
+ fs_1.default.writeFileSync(tempFile, content, 'utf-8');
215
+ // 原子性重命名
216
+ fs_1.default.renameSync(tempFile, filePath);
217
+ }
218
+ catch (error) {
219
+ // 清理临时文件
220
+ if (fs_1.default.existsSync(tempFile)) {
221
+ fs_1.default.unlinkSync(tempFile);
222
+ }
223
+ throw error;
224
+ }
225
+ };
226
+ exports.atomicWriteFile = atomicWriteFile;
227
+ /**
228
+ * 带回退的安全写入
229
+ * 如果写入失败,恢复原文件
230
+ * @param filePath 目标文件路径
231
+ * @param content 要写入的内容
232
+ * @returns 是否成功
233
+ */
234
+ const safeWriteConfig = (filePath, content) => {
235
+ let originalContent = null;
236
+ try {
237
+ // 如果原文件存在,读取其内容
238
+ if (fs_1.default.existsSync(filePath)) {
239
+ originalContent = fs_1.default.readFileSync(filePath, 'utf-8');
240
+ }
241
+ // 原子性写入新内容
242
+ (0, exports.atomicWriteFile)(filePath, content);
243
+ return true;
244
+ }
245
+ catch (error) {
246
+ console.error(`Failed to write ${filePath}:`, error);
247
+ // 如果写入失败且原文件存在,恢复原文件
248
+ if (originalContent !== null && !fs_1.default.existsSync(filePath)) {
249
+ try {
250
+ fs_1.default.writeFileSync(filePath, originalContent, 'utf-8');
251
+ console.log(`Restored ${filePath} from backup`);
252
+ }
253
+ catch (restoreError) {
254
+ console.error(`Failed to restore ${filePath}:`, restoreError);
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ };
260
+ exports.safeWriteConfig = safeWriteConfig;