@minniexcode/codex-switch 0.0.4 → 0.0.6
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 +35 -97
- package/dist/app/add-provider.js +40 -3
- package/dist/app/edit-provider.js +76 -3
- package/dist/app/export-providers.js +2 -2
- package/dist/app/get-current-profile.js +1 -1
- package/dist/app/get-status.js +10 -3
- package/dist/app/import-providers.js +47 -3
- package/dist/app/list-backups.js +1 -1
- package/dist/app/list-config-profiles.js +30 -0
- package/dist/app/list-providers.js +1 -1
- package/dist/app/remove-provider.js +35 -3
- package/dist/app/rollback-backup.js +1 -1
- package/dist/app/rollback-latest.js +1 -1
- package/dist/app/run-doctor.js +44 -26
- package/dist/app/run-mutation.js +2 -2
- package/dist/app/setup-codex.js +37 -20
- package/dist/app/show-config.js +34 -0
- package/dist/app/show-provider.js +1 -1
- package/dist/app/switch-provider.js +8 -5
- package/dist/cli/add-interactive.js +7 -106
- package/dist/cli/args.js +5 -126
- package/dist/cli/help.js +5 -276
- package/dist/cli/interactive.js +16 -171
- package/dist/cli/output.js +23 -1
- package/dist/cli/prompt.js +3 -108
- package/dist/cli.js +10 -315
- package/dist/commands/args.js +132 -0
- package/dist/commands/dispatch.js +16 -0
- package/dist/commands/handlers.js +391 -0
- package/dist/commands/help.js +119 -0
- package/dist/commands/registry.js +291 -0
- package/dist/commands/types.js +2 -0
- package/dist/domain/config.js +548 -39
- package/dist/infra/backup-repo.js +8 -208
- package/dist/infra/codex-cli.js +8 -113
- package/dist/infra/codex-discovery.js +3 -41
- package/dist/infra/codex-paths.js +5 -69
- package/dist/infra/config-repo.js +161 -9
- package/dist/infra/fs-utils.js +7 -95
- package/dist/infra/lock-repo.js +3 -97
- package/dist/infra/providers-repo.js +7 -96
- package/dist/interaction/add-interactive.js +108 -0
- package/dist/interaction/interactive.js +216 -0
- package/dist/interaction/prompt.js +110 -0
- package/dist/runtime/codex-cli.js +130 -0
- package/dist/runtime/codex-probe.js +50 -0
- package/dist/runtime/types.js +2 -0
- package/dist/storage/backup-repo.js +210 -0
- package/dist/storage/codex-paths.js +71 -0
- package/dist/storage/config-repo.js +208 -0
- package/dist/storage/fs-utils.js +97 -0
- package/dist/storage/lock-repo.js +99 -0
- package/dist/storage/providers-repo.js +98 -0
- package/docs/Design/codex-switch-v0.0.5-design.md +932 -0
- package/docs/Design/codex-switch-v0.0.6-design.md +708 -0
- package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +340 -0
- package/docs/PRD/codex-switch-prd-v0.1.0.md +215 -291
- package/docs/PRD/codex-switch-prd.md +1 -1
- package/docs/cli-usage.md +2 -1
- package/docs/codex-switch-technical-architecture.md +73 -4
- package/docs/test-report-0.0.5.md +163 -0
- package/docs/testing.md +131 -0
- package/package.json +1 -1
- /package/docs/{codex-switch-v0.0.4-design.md → Design/codex-switch-v0.0.4-design.md} +0 -0
package/dist/domain/config.js
CHANGED
|
@@ -33,74 +33,583 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_LINE_ENDING = void 0;
|
|
36
37
|
exports.parseTopLevelProfile = parseTopLevelProfile;
|
|
37
38
|
exports.parseProfileNames = parseProfileNames;
|
|
38
39
|
exports.replaceTopLevelProfile = replaceTopLevelProfile;
|
|
40
|
+
exports.parseStructuredConfig = parseStructuredConfig;
|
|
41
|
+
exports.buildManagedProfileViews = buildManagedProfileViews;
|
|
42
|
+
exports.collectConfigConsistencyIssues = collectConfigConsistencyIssues;
|
|
43
|
+
exports.validateManagedProfileCreation = validateManagedProfileCreation;
|
|
44
|
+
exports.planProfileLifecycleOutcome = planProfileLifecycleOutcome;
|
|
45
|
+
exports.planConfigMutation = planConfigMutation;
|
|
46
|
+
exports.applyPatchOperations = applyPatchOperations;
|
|
39
47
|
const os = __importStar(require("node:os"));
|
|
48
|
+
const errors_1 = require("./errors");
|
|
40
49
|
/**
|
|
41
50
|
* Reads the active top-level profile from config.toml content.
|
|
42
51
|
*/
|
|
43
52
|
function parseTopLevelProfile(configContent) {
|
|
53
|
+
return parseStructuredConfig(configContent).activeProfile;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Collects all named profile sections declared in config.toml content.
|
|
57
|
+
*/
|
|
58
|
+
function parseProfileNames(configContent) {
|
|
59
|
+
return new Set(parseStructuredConfig(configContent).profiles.map((profile) => profile.name));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Replaces or inserts the top-level profile assignment while preserving the rest of the file.
|
|
63
|
+
*/
|
|
64
|
+
function replaceTopLevelProfile(configContent, profile) {
|
|
65
|
+
const plan = planConfigMutation(parseStructuredConfig(configContent), { setActiveProfile: profile });
|
|
66
|
+
return applyPatchOperations(configContent, plan.operations);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parses the supported config.toml subset into a structured document with stable text ranges.
|
|
70
|
+
*/
|
|
71
|
+
function parseStructuredConfig(configContent) {
|
|
72
|
+
const lineEnding = configContent.includes("\r\n") ? "\r\n" : "\n";
|
|
73
|
+
const lines = splitWithOffsets(configContent);
|
|
74
|
+
let activeProfile = null;
|
|
75
|
+
let activeProfileRange = null;
|
|
76
|
+
const profiles = [];
|
|
77
|
+
const modelProviders = [];
|
|
78
|
+
let currentProfile = null;
|
|
79
|
+
let currentModelProvider = null;
|
|
44
80
|
let inRoot = true;
|
|
45
|
-
for (const line of
|
|
46
|
-
const trimmed = line.trim();
|
|
47
|
-
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const trimmed = line.content.trim();
|
|
83
|
+
const headerMatch = trimmed.match(/^\[profiles\.([^\]]+)\]$/);
|
|
84
|
+
if (headerMatch) {
|
|
85
|
+
if (currentProfile) {
|
|
86
|
+
currentProfile.sectionEnd = line.start;
|
|
87
|
+
}
|
|
88
|
+
if (currentModelProvider) {
|
|
89
|
+
currentModelProvider.sectionEnd = line.start;
|
|
90
|
+
currentModelProvider = null;
|
|
91
|
+
}
|
|
92
|
+
currentProfile = {
|
|
93
|
+
name: headerMatch[1],
|
|
94
|
+
headerStart: line.start,
|
|
95
|
+
sectionStart: line.start,
|
|
96
|
+
sectionEnd: configContent.length,
|
|
97
|
+
managedFieldInsertIndex: configContent.length,
|
|
98
|
+
modelValueRange: null,
|
|
99
|
+
modelProviderValueRange: null,
|
|
100
|
+
model: null,
|
|
101
|
+
modelProvider: null,
|
|
102
|
+
};
|
|
103
|
+
profiles.push(currentProfile);
|
|
48
104
|
inRoot = false;
|
|
49
105
|
continue;
|
|
50
106
|
}
|
|
51
|
-
|
|
107
|
+
const modelProviderHeaderMatch = trimmed.match(/^\[model_providers\.([^\]]+)\]$/);
|
|
108
|
+
if (modelProviderHeaderMatch) {
|
|
109
|
+
if (currentProfile) {
|
|
110
|
+
currentProfile.sectionEnd = line.start;
|
|
111
|
+
currentProfile = null;
|
|
112
|
+
}
|
|
113
|
+
if (currentModelProvider) {
|
|
114
|
+
currentModelProvider.sectionEnd = line.start;
|
|
115
|
+
}
|
|
116
|
+
currentModelProvider = {
|
|
117
|
+
name: modelProviderHeaderMatch[1],
|
|
118
|
+
sectionStart: line.start,
|
|
119
|
+
sectionEnd: configContent.length,
|
|
120
|
+
baseUrlValueRange: null,
|
|
121
|
+
baseUrl: null,
|
|
122
|
+
};
|
|
123
|
+
modelProviders.push(currentModelProvider);
|
|
124
|
+
inRoot = false;
|
|
52
125
|
continue;
|
|
53
126
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
127
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
128
|
+
if (currentProfile) {
|
|
129
|
+
currentProfile.sectionEnd = line.start;
|
|
130
|
+
currentProfile = null;
|
|
131
|
+
}
|
|
132
|
+
if (currentModelProvider) {
|
|
133
|
+
currentModelProvider.sectionEnd = line.start;
|
|
134
|
+
currentModelProvider = null;
|
|
135
|
+
}
|
|
136
|
+
inRoot = false;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (inRoot) {
|
|
140
|
+
const profileMatch = matchKeyValueLine(line.content, "profile");
|
|
141
|
+
if (profileMatch && !activeProfile) {
|
|
142
|
+
activeProfile = profileMatch.value;
|
|
143
|
+
activeProfileRange = {
|
|
144
|
+
start: line.start + profileMatch.valueStart,
|
|
145
|
+
end: line.start + profileMatch.valueEnd,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (currentProfile) {
|
|
150
|
+
const modelMatch = matchKeyValueLine(line.content, "model");
|
|
151
|
+
if (modelMatch) {
|
|
152
|
+
currentProfile.model = modelMatch.value;
|
|
153
|
+
currentProfile.modelValueRange = {
|
|
154
|
+
start: line.start + modelMatch.valueStart,
|
|
155
|
+
end: line.start + modelMatch.valueEnd,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const modelProviderMatch = matchKeyValueLine(line.content, "model_provider");
|
|
159
|
+
if (modelProviderMatch) {
|
|
160
|
+
currentProfile.modelProvider = modelProviderMatch.value;
|
|
161
|
+
currentProfile.modelProviderValueRange = {
|
|
162
|
+
start: line.start + modelProviderMatch.valueStart,
|
|
163
|
+
end: line.start + modelProviderMatch.valueEnd,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (currentModelProvider) {
|
|
168
|
+
const baseUrlMatch = matchKeyValueLine(line.content, "base_url");
|
|
169
|
+
if (baseUrlMatch) {
|
|
170
|
+
currentModelProvider.baseUrl = baseUrlMatch.value;
|
|
171
|
+
currentModelProvider.baseUrlValueRange = {
|
|
172
|
+
start: line.start + baseUrlMatch.valueStart,
|
|
173
|
+
end: line.start + baseUrlMatch.valueEnd,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
57
176
|
}
|
|
58
177
|
}
|
|
59
|
-
return
|
|
178
|
+
return {
|
|
179
|
+
rawText: configContent,
|
|
180
|
+
lineEnding,
|
|
181
|
+
activeProfile,
|
|
182
|
+
activeProfileRange,
|
|
183
|
+
profiles: profiles.map((profile) => ({
|
|
184
|
+
...profile,
|
|
185
|
+
managedFieldInsertIndex: findManagedFieldInsertIndex(configContent, profile.sectionStart, profile.sectionEnd),
|
|
186
|
+
})),
|
|
187
|
+
modelProviders,
|
|
188
|
+
};
|
|
60
189
|
}
|
|
61
190
|
/**
|
|
62
|
-
*
|
|
191
|
+
* Builds the managed/unmanaged/orphaned profile views used by config commands and diagnostics.
|
|
63
192
|
*/
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
193
|
+
function buildManagedProfileViews(document, providers) {
|
|
194
|
+
const linkMap = buildProfileLinkMap(providers);
|
|
195
|
+
const modelProviderMap = new Map(document.modelProviders.map((provider) => [provider.name, provider]));
|
|
196
|
+
const views = [];
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
for (const section of document.profiles) {
|
|
199
|
+
const linkInfo = linkMap.get(section.name) ?? { linkedProviders: [], managed: false };
|
|
200
|
+
const modelProviderSection = section.modelProvider ? modelProviderMap.get(section.modelProvider) ?? null : null;
|
|
201
|
+
seen.add(section.name);
|
|
202
|
+
views.push({
|
|
203
|
+
name: section.name,
|
|
204
|
+
managed: linkInfo.managed,
|
|
205
|
+
isActive: document.activeProfile === section.name,
|
|
206
|
+
linkedProviders: [...linkInfo.linkedProviders].sort(),
|
|
207
|
+
model: section.model,
|
|
208
|
+
modelProvider: section.modelProvider,
|
|
209
|
+
baseUrl: modelProviderSection?.baseUrl ?? null,
|
|
210
|
+
managedFields: collectManagedFields(section.model, section.modelProvider),
|
|
211
|
+
source: linkInfo.managed ? "managed" : "unmanaged",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
for (const [profile, linkInfo] of [...linkMap.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
215
|
+
if (seen.has(profile)) {
|
|
216
|
+
continue;
|
|
71
217
|
}
|
|
218
|
+
views.push({
|
|
219
|
+
name: profile,
|
|
220
|
+
managed: true,
|
|
221
|
+
isActive: document.activeProfile === profile,
|
|
222
|
+
linkedProviders: [...linkInfo.linkedProviders].sort(),
|
|
223
|
+
model: null,
|
|
224
|
+
modelProvider: null,
|
|
225
|
+
baseUrl: null,
|
|
226
|
+
managedFields: [],
|
|
227
|
+
source: "orphaned-reference",
|
|
228
|
+
});
|
|
72
229
|
}
|
|
73
|
-
return
|
|
230
|
+
return views.sort((left, right) => left.name.localeCompare(right.name));
|
|
74
231
|
}
|
|
75
232
|
/**
|
|
76
|
-
*
|
|
233
|
+
* Collects structured config consistency issues for doctor and status.
|
|
77
234
|
*/
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
235
|
+
function collectConfigConsistencyIssues(document, providers) {
|
|
236
|
+
const issues = [];
|
|
237
|
+
for (const view of buildManagedProfileViews(document, providers)) {
|
|
238
|
+
if (view.source === "orphaned-reference") {
|
|
239
|
+
issues.push({
|
|
240
|
+
code: "ORPHANED_PROFILE_REFERENCE",
|
|
241
|
+
profile: view.name,
|
|
242
|
+
providers: [...view.linkedProviders],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (view.source === "unmanaged" && view.linkedProviders.length === 0) {
|
|
246
|
+
issues.push({
|
|
247
|
+
code: "ORPHANED_PROFILE_SECTION",
|
|
248
|
+
profile: view.name,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (view.linkedProviders.length > 1) {
|
|
252
|
+
issues.push({
|
|
253
|
+
code: "SHARED_PROFILE_REFERENCE",
|
|
254
|
+
profile: view.name,
|
|
255
|
+
providers: [...view.linkedProviders],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (view.source !== "orphaned-reference") {
|
|
259
|
+
if (!view.modelProvider) {
|
|
260
|
+
issues.push({
|
|
261
|
+
code: "MODEL_PROVIDER_MISSING",
|
|
262
|
+
profile: view.name,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
if (view.modelProvider !== view.name) {
|
|
267
|
+
issues.push({
|
|
268
|
+
code: "MODEL_PROVIDER_NAME_MISMATCH",
|
|
269
|
+
profile: view.name,
|
|
270
|
+
modelProvider: view.modelProvider,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const modelProviderSection = document.modelProviders.find((entry) => entry.name === view.modelProvider);
|
|
274
|
+
if (!modelProviderSection) {
|
|
275
|
+
issues.push({
|
|
276
|
+
code: "MODEL_PROVIDER_SECTION_MISSING",
|
|
277
|
+
profile: view.name,
|
|
278
|
+
modelProvider: view.modelProvider,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else if (!modelProviderSection.baseUrl) {
|
|
282
|
+
issues.push({
|
|
283
|
+
code: "MODEL_PROVIDER_BASE_URL_MISSING",
|
|
284
|
+
profile: view.name,
|
|
285
|
+
modelProvider: view.modelProvider,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
88
289
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
290
|
+
}
|
|
291
|
+
if (document.activeProfile) {
|
|
292
|
+
const activeLinkInfo = buildProfileLinkMap(providers).get(document.activeProfile);
|
|
293
|
+
if (!activeLinkInfo) {
|
|
294
|
+
issues.push({
|
|
295
|
+
code: "UNMANAGED_ACTIVE_PROFILE",
|
|
296
|
+
profile: document.activeProfile,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return issues.sort((left, right) => {
|
|
301
|
+
if (left.profile === right.profile) {
|
|
302
|
+
return left.code.localeCompare(right.code);
|
|
92
303
|
}
|
|
93
|
-
return
|
|
304
|
+
return left.profile.localeCompare(right.profile);
|
|
94
305
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Ensures the minimal managed profile fields are available before a new section is created.
|
|
309
|
+
*/
|
|
310
|
+
function validateManagedProfileCreation(profile, fields) {
|
|
311
|
+
const model = fields.model?.trim() ?? "";
|
|
312
|
+
const modelProvider = fields.modelProvider?.trim() ?? "";
|
|
313
|
+
if (!model || !modelProvider) {
|
|
314
|
+
throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Managed profile "${profile}" requires both model and model_provider.`, {
|
|
315
|
+
profile,
|
|
316
|
+
missingFields: [
|
|
317
|
+
!model ? "model" : null,
|
|
318
|
+
!modelProvider ? "model_provider" : null,
|
|
319
|
+
].filter((value) => Boolean(value)),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
model,
|
|
324
|
+
modelProvider,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
|
|
329
|
+
*/
|
|
330
|
+
function planProfileLifecycleOutcome(args) {
|
|
331
|
+
if (!args.oldProfile || args.oldProfile === args.newProfile) {
|
|
332
|
+
return {
|
|
333
|
+
deletedProfileSections: [],
|
|
334
|
+
keptSharedProfiles: [],
|
|
335
|
+
switchedActiveProfile: false,
|
|
336
|
+
nextActiveProfile: args.activeProfile,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const remainingLinks = args.remainingLinksByProfile.get(args.oldProfile) ?? [];
|
|
340
|
+
if (remainingLinks.length > 0) {
|
|
341
|
+
return {
|
|
342
|
+
deletedProfileSections: [],
|
|
343
|
+
keptSharedProfiles: [args.oldProfile],
|
|
344
|
+
switchedActiveProfile: false,
|
|
345
|
+
nextActiveProfile: args.activeProfile,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (args.activeProfile === args.oldProfile) {
|
|
349
|
+
if (!args.switchToProfile) {
|
|
350
|
+
throw (0, errors_1.cliError)("PROFILE_IN_USE", `Profile "${args.oldProfile}" is still the active profile. Switch first before removing the last linked provider.`, {
|
|
351
|
+
profile: args.oldProfile,
|
|
352
|
+
provider: args.providerName,
|
|
353
|
+
activeProfile: args.activeProfile,
|
|
354
|
+
linkedProviders: [],
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
deletedProfileSections: [args.oldProfile],
|
|
359
|
+
keptSharedProfiles: [],
|
|
360
|
+
switchedActiveProfile: true,
|
|
361
|
+
nextActiveProfile: args.switchToProfile,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
deletedProfileSections: [args.oldProfile],
|
|
366
|
+
keptSharedProfiles: [],
|
|
367
|
+
switchedActiveProfile: false,
|
|
368
|
+
nextActiveProfile: args.activeProfile,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Builds a text patch plan for top-level profile changes and profile section lifecycle changes.
|
|
373
|
+
*/
|
|
374
|
+
function planConfigMutation(document, args) {
|
|
375
|
+
const operations = [];
|
|
376
|
+
const createdProfileSections = [];
|
|
377
|
+
const deletedProfileSections = [];
|
|
378
|
+
const updatedProfiles = [];
|
|
379
|
+
const sectionMap = new Map(document.profiles.map((profile) => [profile.name, profile]));
|
|
380
|
+
if (args.setActiveProfile && args.setActiveProfile !== document.activeProfile) {
|
|
381
|
+
const quoted = `"${args.setActiveProfile}"`;
|
|
382
|
+
if (document.activeProfileRange) {
|
|
383
|
+
operations.push({
|
|
384
|
+
kind: "replace-range",
|
|
385
|
+
start: document.activeProfileRange.start,
|
|
386
|
+
end: document.activeProfileRange.end,
|
|
387
|
+
text: quoted,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const insertAt = findTopLevelInsertIndex(document.rawText);
|
|
392
|
+
const text = `profile = ${quoted}${document.lineEnding}`;
|
|
393
|
+
operations.push({
|
|
394
|
+
kind: "insert-at",
|
|
395
|
+
index: insertAt,
|
|
396
|
+
text,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
for (const profileName of args.deleteProfiles ?? []) {
|
|
401
|
+
const section = sectionMap.get(profileName);
|
|
402
|
+
if (!section) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
operations.push({
|
|
406
|
+
kind: "delete-range",
|
|
407
|
+
start: section.sectionStart,
|
|
408
|
+
end: expandDeletionEnd(document.rawText, section.sectionStart, section.sectionEnd),
|
|
409
|
+
});
|
|
410
|
+
deletedProfileSections.push(profileName);
|
|
411
|
+
}
|
|
412
|
+
for (const [profileName, fields] of Object.entries(args.upsertProfiles ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
413
|
+
const section = sectionMap.get(profileName);
|
|
414
|
+
if (!section) {
|
|
415
|
+
const requiredFields = validateManagedProfileCreation(profileName, fields);
|
|
416
|
+
const prefix = document.rawText.length > 0 && !document.rawText.endsWith(document.lineEnding)
|
|
417
|
+
? document.lineEnding
|
|
418
|
+
: "";
|
|
419
|
+
operations.push({
|
|
420
|
+
kind: "insert-at",
|
|
421
|
+
index: document.rawText.length,
|
|
422
|
+
text: `${prefix}[profiles.${profileName}]${document.lineEnding}` +
|
|
423
|
+
`model = ${JSON.stringify(requiredFields.model)}${document.lineEnding}` +
|
|
424
|
+
`model_provider = ${JSON.stringify(requiredFields.modelProvider)}${document.lineEnding}`,
|
|
425
|
+
});
|
|
426
|
+
createdProfileSections.push(profileName);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const sectionUpdated = planSectionFieldMutation(document, section, fields, operations);
|
|
430
|
+
if (sectionUpdated) {
|
|
431
|
+
updatedProfiles.push(profileName);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
operations,
|
|
436
|
+
createdProfileSections,
|
|
437
|
+
deletedProfileSections,
|
|
438
|
+
updatedProfiles,
|
|
439
|
+
switchedActiveProfile: Boolean(args.setActiveProfile && args.setActiveProfile !== document.activeProfile),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Applies a patch plan to raw config text. Callers should sort by reverse offsets only once here.
|
|
444
|
+
*/
|
|
445
|
+
function applyPatchOperations(rawText, operations) {
|
|
446
|
+
const sorted = [...operations].sort((left, right) => getOperationStart(right) - getOperationStart(left));
|
|
447
|
+
let nextText = rawText;
|
|
448
|
+
for (const operation of sorted) {
|
|
449
|
+
if (operation.kind === "replace-range") {
|
|
450
|
+
nextText = `${nextText.slice(0, operation.start)}${operation.text}${nextText.slice(operation.end)}`;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (operation.kind === "delete-range") {
|
|
454
|
+
nextText = `${nextText.slice(0, operation.start)}${nextText.slice(operation.end)}`;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
nextText = `${nextText.slice(0, operation.index)}${operation.text}${nextText.slice(operation.index)}`;
|
|
458
|
+
}
|
|
459
|
+
return nextText;
|
|
460
|
+
}
|
|
461
|
+
function planSectionFieldMutation(document, section, fields, operations) {
|
|
462
|
+
let updated = false;
|
|
463
|
+
const modelText = fields.model !== undefined ? JSON.stringify(fields.model) : null;
|
|
464
|
+
const modelProviderText = fields.modelProvider !== undefined ? JSON.stringify(fields.modelProvider) : null;
|
|
465
|
+
const inserts = [];
|
|
466
|
+
if (modelText !== null && section.modelValueRange) {
|
|
467
|
+
if (section.model !== fields.model) {
|
|
468
|
+
operations.push({
|
|
469
|
+
kind: "replace-range",
|
|
470
|
+
start: section.modelValueRange.start,
|
|
471
|
+
end: section.modelValueRange.end,
|
|
472
|
+
text: modelText,
|
|
473
|
+
});
|
|
474
|
+
updated = true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else if (modelText !== null && !section.modelValueRange) {
|
|
478
|
+
inserts.push(`model = ${modelText}${document.lineEnding}`);
|
|
479
|
+
updated = true;
|
|
480
|
+
}
|
|
481
|
+
if (modelProviderText !== null && section.modelProviderValueRange) {
|
|
482
|
+
if (section.modelProvider !== fields.modelProvider) {
|
|
483
|
+
operations.push({
|
|
484
|
+
kind: "replace-range",
|
|
485
|
+
start: section.modelProviderValueRange.start,
|
|
486
|
+
end: section.modelProviderValueRange.end,
|
|
487
|
+
text: modelProviderText,
|
|
488
|
+
});
|
|
489
|
+
updated = true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else if (modelProviderText !== null && !section.modelProviderValueRange) {
|
|
493
|
+
inserts.push(`model_provider = ${modelProviderText}${document.lineEnding}`);
|
|
494
|
+
updated = true;
|
|
495
|
+
}
|
|
496
|
+
if (inserts.length > 0) {
|
|
497
|
+
operations.push({
|
|
498
|
+
kind: "insert-at",
|
|
499
|
+
index: section.managedFieldInsertIndex,
|
|
500
|
+
text: inserts.join(""),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return updated;
|
|
504
|
+
}
|
|
505
|
+
function splitWithOffsets(value) {
|
|
506
|
+
if (value.length === 0) {
|
|
507
|
+
return [];
|
|
508
|
+
}
|
|
509
|
+
const result = [];
|
|
510
|
+
let index = 0;
|
|
511
|
+
while (index < value.length) {
|
|
512
|
+
let nextBreak = value.indexOf("\n", index);
|
|
513
|
+
if (nextBreak === -1) {
|
|
514
|
+
nextBreak = value.length;
|
|
100
515
|
}
|
|
101
516
|
else {
|
|
102
|
-
|
|
517
|
+
nextBreak += 1;
|
|
518
|
+
}
|
|
519
|
+
result.push({
|
|
520
|
+
content: value.slice(index, nextBreak).replace(/\r?\n$/, ""),
|
|
521
|
+
start: index,
|
|
522
|
+
end: nextBreak,
|
|
523
|
+
});
|
|
524
|
+
index = nextBreak;
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
function matchKeyValueLine(line, key) {
|
|
529
|
+
const match = line.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*(["'])(.*?)\\1\\s*(#.*)?$`));
|
|
530
|
+
if (!match || match.index === undefined) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
const value = match[2];
|
|
534
|
+
const openingQuoteIndex = line.indexOf(match[1], match.index);
|
|
535
|
+
if (openingQuoteIndex === -1) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const valueStart = openingQuoteIndex;
|
|
539
|
+
const valueEnd = openingQuoteIndex + match[1].length + value.length + match[1].length;
|
|
540
|
+
return {
|
|
541
|
+
value,
|
|
542
|
+
valueStart,
|
|
543
|
+
valueEnd,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function findManagedFieldInsertIndex(rawText, sectionStart, sectionEnd) {
|
|
547
|
+
const sectionText = rawText.slice(sectionStart, sectionEnd);
|
|
548
|
+
const lines = splitWithOffsets(sectionText);
|
|
549
|
+
let lastMeaningfulIndex = lines.length - 1;
|
|
550
|
+
while (lastMeaningfulIndex >= 0) {
|
|
551
|
+
const trimmed = lines[lastMeaningfulIndex].content.trim();
|
|
552
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
553
|
+
lastMeaningfulIndex -= 1;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
if (lastMeaningfulIndex < 0) {
|
|
559
|
+
return sectionEnd;
|
|
560
|
+
}
|
|
561
|
+
return sectionStart + lines[lastMeaningfulIndex].end;
|
|
562
|
+
}
|
|
563
|
+
function collectManagedFields(model, modelProvider) {
|
|
564
|
+
const fields = [];
|
|
565
|
+
if (model !== null) {
|
|
566
|
+
fields.push("model");
|
|
567
|
+
}
|
|
568
|
+
if (modelProvider !== null) {
|
|
569
|
+
fields.push("model_provider");
|
|
570
|
+
}
|
|
571
|
+
return fields;
|
|
572
|
+
}
|
|
573
|
+
function buildProfileLinkMap(providers) {
|
|
574
|
+
const map = new Map();
|
|
575
|
+
for (const [providerName, provider] of Object.entries(providers?.providers ?? {})) {
|
|
576
|
+
const current = map.get(provider.profile) ?? { linkedProviders: [], managed: true };
|
|
577
|
+
current.linkedProviders.push(providerName);
|
|
578
|
+
current.managed = true;
|
|
579
|
+
map.set(provider.profile, current);
|
|
580
|
+
}
|
|
581
|
+
for (const value of map.values()) {
|
|
582
|
+
value.linkedProviders.sort();
|
|
583
|
+
}
|
|
584
|
+
return map;
|
|
585
|
+
}
|
|
586
|
+
function getOperationStart(operation) {
|
|
587
|
+
if (operation.kind === "insert-at") {
|
|
588
|
+
return operation.index;
|
|
589
|
+
}
|
|
590
|
+
return operation.start;
|
|
591
|
+
}
|
|
592
|
+
function findTopLevelInsertIndex(rawText) {
|
|
593
|
+
const sectionMatch = rawText.match(/^\s*\[/m);
|
|
594
|
+
return sectionMatch && sectionMatch.index !== undefined ? sectionMatch.index : rawText.length;
|
|
595
|
+
}
|
|
596
|
+
function expandDeletionEnd(rawText, sectionStart, sectionEnd) {
|
|
597
|
+
let end = sectionEnd;
|
|
598
|
+
while (end < rawText.length && (rawText[end] === "\r" || rawText[end] === "\n")) {
|
|
599
|
+
end += 1;
|
|
600
|
+
}
|
|
601
|
+
if (sectionStart > 0) {
|
|
602
|
+
let cursor = sectionStart - 1;
|
|
603
|
+
while (cursor >= 0 && (rawText[cursor] === "\r" || rawText[cursor] === "\n")) {
|
|
604
|
+
cursor -= 1;
|
|
605
|
+
}
|
|
606
|
+
if (cursor < sectionStart - 1) {
|
|
607
|
+
return end;
|
|
103
608
|
}
|
|
104
609
|
}
|
|
105
|
-
return
|
|
610
|
+
return end;
|
|
611
|
+
}
|
|
612
|
+
function escapeRegExp(value) {
|
|
613
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
106
614
|
}
|
|
615
|
+
exports.DEFAULT_LINE_ENDING = os.EOL === "\r\n" ? "\r\n" : "\n";
|