cabletidy 0.1.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/LICENSE +21 -0
- package/README.md +230 -0
- package/bin/cabletidy.mjs +2 -0
- package/package.json +40 -0
- package/src/cli.mjs +141 -0
- package/src/codex-catalog.mjs +227 -0
- package/src/codex-config-file.mjs +67 -0
- package/src/codex-native-provider.mjs +311 -0
- package/src/config.mjs +361 -0
- package/src/model-resolver.mjs +187 -0
- package/src/server.mjs +1269 -0
- package/src/target-artifacts.mjs +177 -0
- package/src/validation.mjs +387 -0
- package/web/app.js +1642 -0
- package/web/config-identity.js +73 -0
- package/web/index.html +48 -0
- package/web/styles.css +1188 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getStaticTOMLValue, parseTOML } from "toml-eslint-parser";
|
|
2
|
+
|
|
3
|
+
export function parseCodexConfig(text) {
|
|
4
|
+
return parseTOML(text, { tomlVersion: "1.0.0" });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function readCodexConfig(text) {
|
|
8
|
+
return getStaticTOMLValue(parseCodexConfig(text));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function keyParts(node) {
|
|
12
|
+
return node.key.keys.map((key) => key.type === "TOMLBare" ? key.name : key.value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function patchRootConfig(text, values) {
|
|
16
|
+
const body = parseCodexConfig(text).body[0].body;
|
|
17
|
+
const edits = [];
|
|
18
|
+
const additions = [];
|
|
19
|
+
for (const [key, value] of Object.entries(values)) {
|
|
20
|
+
const node = body.find((item) => item.type === "TOMLKeyValue" && keyParts(item).length === 1 && keyParts(item)[0] === key);
|
|
21
|
+
if (node) {
|
|
22
|
+
edits.push({ start: value === undefined ? node.range[0] : node.value.range[0], end: node.range[1], text: value === undefined ? "" : JSON.stringify(value) });
|
|
23
|
+
} else if (value !== undefined) additions.push(`${key} = ${JSON.stringify(value)}\n`);
|
|
24
|
+
}
|
|
25
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
26
|
+
text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
|
|
27
|
+
}
|
|
28
|
+
const result = additions.join("") + text;
|
|
29
|
+
parseCodexConfig(result);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function patchProviderConfig(text, providerId, providerContents) {
|
|
34
|
+
const ast = parseCodexConfig(text);
|
|
35
|
+
const edits = [];
|
|
36
|
+
for (const node of ast.body[0].body) {
|
|
37
|
+
if (node.type !== "TOMLTable") continue;
|
|
38
|
+
const parts = node.resolvedKey || keyParts(node);
|
|
39
|
+
if (parts[0] === "model_providers" && parts[1] === providerId) {
|
|
40
|
+
edits.push({ start: node.range[0], end: node.range[1] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
44
|
+
text = text.slice(0, edit.start) + text.slice(edit.end);
|
|
45
|
+
}
|
|
46
|
+
// Remove only real marker comments, never matching text inside instructions.
|
|
47
|
+
const markers = parseCodexConfig(text).comments.filter((comment) =>
|
|
48
|
+
["# >>> CABLETIDY MANAGED PROVIDER", "# <<< CABLETIDY MANAGED PROVIDER"].some((marker) =>
|
|
49
|
+
text.slice(...comment.range).startsWith(`${marker} ${providerId} `),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
for (const comment of markers.sort((a, b) => b.range[0] - a.range[0])) {
|
|
53
|
+
text = text.slice(0, comment.range[0]) + text.slice(comment.range[1]);
|
|
54
|
+
}
|
|
55
|
+
const updatedAst = parseCodexConfig(text);
|
|
56
|
+
const firstProvider = updatedAst.body[0].body.find((node) => {
|
|
57
|
+
if (node.type !== "TOMLTable") return false;
|
|
58
|
+
const parts = node.resolvedKey || keyParts(node);
|
|
59
|
+
return parts[0] === "model_providers";
|
|
60
|
+
});
|
|
61
|
+
const providerBlock = `${providerContents.trimEnd()}\n\n`;
|
|
62
|
+
const result = firstProvider
|
|
63
|
+
? `${text.slice(0, firstProvider.range[0]).trimEnd()}\n\n${providerBlock}${text.slice(firstProvider.range[0])}`
|
|
64
|
+
: `${text.trimEnd()}\n\n${providerContents}`;
|
|
65
|
+
parseCodexConfig(result);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
backupFile,
|
|
8
|
+
normalizeConfig,
|
|
9
|
+
resolveUpstreamSecret,
|
|
10
|
+
} from "./config.mjs";
|
|
11
|
+
import { configurationId, providerIdForConfiguration, configurationBaseUrl } from "../web/config-identity.js";
|
|
12
|
+
import { resolveModelProfile } from "./model-resolver.mjs";
|
|
13
|
+
import { loadCodexCatalog, planCodexCatalog, validateCatalog } from "./codex-catalog.mjs";
|
|
14
|
+
import { patchRootConfig, patchProviderConfig, readCodexConfig } from "./codex-config-file.mjs";
|
|
15
|
+
|
|
16
|
+
export const CODEX_NATIVE_PROVIDER_INTEGRATION = "codex-native-provider";
|
|
17
|
+
export const CODEX_TARGET_FORMAT = "codex.config.toml.v1";
|
|
18
|
+
export const CODEX_NATIVE_REQUIRED_ENDPOINTS = Object.freeze([
|
|
19
|
+
Object.freeze({
|
|
20
|
+
method: "GET",
|
|
21
|
+
path: "/v1/models",
|
|
22
|
+
purpose: "model_discovery",
|
|
23
|
+
}),
|
|
24
|
+
Object.freeze({
|
|
25
|
+
method: "POST",
|
|
26
|
+
path: "/v1/responses",
|
|
27
|
+
purpose: "responses",
|
|
28
|
+
supportsStreaming: true,
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const ROOT_BEGIN = "# >>> CABLETIDY MANAGED PROVIDER";
|
|
33
|
+
const ROOT_END = "# <<< CABLETIDY MANAGED PROVIDER";
|
|
34
|
+
const ACTIVE_BEGIN = "# >>> CABLETIDY MANAGED ACTIVE PROVIDER";
|
|
35
|
+
const ACTIVE_END = "# <<< CABLETIDY MANAGED ACTIVE PROVIDER";
|
|
36
|
+
|
|
37
|
+
function quote(value) {
|
|
38
|
+
return JSON.stringify(String(value ?? ""));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function providerForBinding(config, binding, virtualProvider) {
|
|
42
|
+
const route = config.routes?.[virtualProvider?.route];
|
|
43
|
+
if (route?.backends?.length !== 1) throw new Error("每份配置必须且只能连接一个 upstream");
|
|
44
|
+
return config.upstreams?.[route.backends[0]?.upstream];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function profilePolicy(profile) {
|
|
48
|
+
return {
|
|
49
|
+
contextWindow: Number(profile?.contextWindow || 0) || null,
|
|
50
|
+
compact: profile?.compact || null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildCodexArtifacts(config, options = {}, secrets = {}) {
|
|
55
|
+
// Collisions can block a chain of renames, so reject them before looking up a new ID.
|
|
56
|
+
const names = new Set();
|
|
57
|
+
for (const [id, binding] of Object.entries(config.bindings || {})) {
|
|
58
|
+
const nameId = configurationId(binding?.name, id);
|
|
59
|
+
if (names.has(nameId)) throw new Error(`配置名称规范化后重复: ${nameId}`);
|
|
60
|
+
names.add(nameId);
|
|
61
|
+
}
|
|
62
|
+
let bindingId = options.bindingId;
|
|
63
|
+
const requestedBinding = config.bindings?.[bindingId];
|
|
64
|
+
config = normalizeConfig(config);
|
|
65
|
+
if (requestedBinding) bindingId = configurationId(requestedBinding.name, bindingId);
|
|
66
|
+
if (!bindingId || !config.bindings?.[bindingId]) {
|
|
67
|
+
bindingId = Object.entries(config.bindings || {}).find(
|
|
68
|
+
([id, item]) =>
|
|
69
|
+
item.target === "codex" &&
|
|
70
|
+
(!options.bindingId || id === options.bindingId || item.id === options.bindingId),
|
|
71
|
+
)?.[0];
|
|
72
|
+
}
|
|
73
|
+
const binding = bindingId ? config.bindings?.[bindingId] : null;
|
|
74
|
+
if (!binding) throw new Error("找不到 Codex binding");
|
|
75
|
+
const virtualProvider = config.virtualProviders?.[binding.virtualProvider];
|
|
76
|
+
if (!virtualProvider) throw new Error("Binding 引用的 Virtual Provider 不存在");
|
|
77
|
+
if (virtualProvider.id !== providerIdForConfiguration(bindingId) ||
|
|
78
|
+
Object.values(config.bindings).filter((item) => item?.virtualProvider === virtualProvider.id).length !== 1) {
|
|
79
|
+
throw new Error("配置与 Virtual Provider 必须一对一,Virtual Provider ID 必须为 cabletidy_<配置ID>");
|
|
80
|
+
}
|
|
81
|
+
if (binding.target !== "codex") {
|
|
82
|
+
throw new Error(`binding ${bindingId} 不是 Codex target`);
|
|
83
|
+
}
|
|
84
|
+
const defaultModel = binding.defaultModel || virtualProvider.defaultModel;
|
|
85
|
+
const modelResolution = defaultModel ? resolveModelProfile(config, virtualProvider, defaultModel) : null;
|
|
86
|
+
|
|
87
|
+
const upstream = providerForBinding(config, binding, virtualProvider);
|
|
88
|
+
if (!upstream) throw new Error("找不到 binding 对应的 upstream");
|
|
89
|
+
const providerId = virtualProvider.id;
|
|
90
|
+
const clientModel =
|
|
91
|
+
modelResolution?.profile?.clientModelId ||
|
|
92
|
+
modelResolution?.profile?.aliases?.[0] ||
|
|
93
|
+
modelResolution?.clientModelId || null;
|
|
94
|
+
const policy = profilePolicy(modelResolution?.profile);
|
|
95
|
+
const activeContents = [
|
|
96
|
+
`${ACTIVE_BEGIN} ${providerId} -->`,
|
|
97
|
+
`model_provider = ${quote(providerId)}`,
|
|
98
|
+
...(clientModel ? [`model = ${quote(clientModel)}`] : []),
|
|
99
|
+
`${ACTIVE_END} ${providerId} <--`,
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
const providerContents = [
|
|
103
|
+
`${ROOT_BEGIN} ${providerId} -->`,
|
|
104
|
+
`[model_providers.${providerId}]`,
|
|
105
|
+
`name = ${quote(`CableTidy / ${upstream.name || upstream.id}`)}`,
|
|
106
|
+
`base_url = ${quote(`${configurationBaseUrl(config, bindingId)}/v1`)}`,
|
|
107
|
+
`wire_api = "responses"`,
|
|
108
|
+
`requires_openai_auth = false`,
|
|
109
|
+
`${ROOT_END} ${providerId} <--`,
|
|
110
|
+
"",
|
|
111
|
+
].join("\n");
|
|
112
|
+
const rootContents = `${activeContents}\n${providerContents}`;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
integration: CODEX_NATIVE_PROVIDER_INTEGRATION,
|
|
116
|
+
targetFormat: CODEX_TARGET_FORMAT,
|
|
117
|
+
format: CODEX_TARGET_FORMAT,
|
|
118
|
+
target: "codex",
|
|
119
|
+
mode: "managed_proxy",
|
|
120
|
+
bindingId,
|
|
121
|
+
virtualProviderId: virtualProvider.id,
|
|
122
|
+
providerId,
|
|
123
|
+
activeContents,
|
|
124
|
+
providerContents,
|
|
125
|
+
clientModelId: clientModel,
|
|
126
|
+
modelPolicy: policy,
|
|
127
|
+
files: [{ path: "config.toml", contents: rootContents }],
|
|
128
|
+
environment: { vars: {}, shell: "" },
|
|
129
|
+
upstream: {
|
|
130
|
+
id: upstream.id,
|
|
131
|
+
baseUrl: upstream.baseUrl,
|
|
132
|
+
secretConfigured: Boolean(resolveUpstreamSecret(upstream, secrets)),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function prepareCodexArtifacts(config, options = {}, secrets = {}) {
|
|
138
|
+
const artifacts = buildCodexArtifacts(config, options, secrets);
|
|
139
|
+
config = normalizeConfig(config);
|
|
140
|
+
const provider = config.virtualProviders[config.bindings[artifacts.bindingId].virtualProvider];
|
|
141
|
+
artifacts.catalogPlan = provider.allowedModels?.length
|
|
142
|
+
? planCodexCatalog(config, provider, await (options.loadCatalog || loadCodexCatalog)())
|
|
143
|
+
: { catalog: null, models: [], overrides: [], warnings: [] };
|
|
144
|
+
return stageCodexArtifacts(artifacts, options);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const CATALOG_STATE_FILE = ".cabletidy-model-catalog.json";
|
|
148
|
+
|
|
149
|
+
async function readOptional(file) {
|
|
150
|
+
try { return await fs.readFile(file, "utf8"); }
|
|
151
|
+
catch (error) { if (error.code === "ENOENT") return ""; throw error; }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function codexHomeFor(options) {
|
|
155
|
+
return path.resolve(options.codexHome || process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function stageCodexArtifacts(artifacts, options = {}) {
|
|
159
|
+
const codexHome = codexHomeFor(options);
|
|
160
|
+
const existing = await readOptional(path.join(codexHome, "config.toml"));
|
|
161
|
+
const current = readCodexConfig(existing);
|
|
162
|
+
const warnings = [...(artifacts.catalogPlan?.warnings || [])];
|
|
163
|
+
const stateText = await readOptional(path.join(codexHome, CATALOG_STATE_FILE));
|
|
164
|
+
const saved = stateText ? JSON.parse(stateText) : null;
|
|
165
|
+
const ownsCatalog = saved?.version === 1 && current.model_catalog_json === saved.managedPath;
|
|
166
|
+
const previousCatalog = ownsCatalog ? saved.previousCatalog : current.model_catalog_json;
|
|
167
|
+
const previousContext = ownsCatalog ? current.model_context_window ?? saved.previousContext : current.model_context_window;
|
|
168
|
+
const rootValues = {
|
|
169
|
+
model_provider: artifacts.providerId,
|
|
170
|
+
...(artifacts.clientModelId ? { model: artifacts.clientModelId } : {}),
|
|
171
|
+
};
|
|
172
|
+
const files = [];
|
|
173
|
+
let catalogState;
|
|
174
|
+
if (artifacts.catalogPlan?.catalog) {
|
|
175
|
+
warnings.push("模型目录覆盖作用于当前 Codex 配置,不按 provider 隔离;切换到其他 provider 前需恢复官方定义或原目录。重新启动 Codex 后读取更新。");
|
|
176
|
+
let catalog = structuredClone(artifacts.catalogPlan.catalog);
|
|
177
|
+
if (previousCatalog) {
|
|
178
|
+
if (typeof previousCatalog !== "string") throw new Error("现有 model_catalog_json 必须为路径字符串");
|
|
179
|
+
const file = path.isAbsolute(previousCatalog) ? previousCatalog : path.resolve(codexHome, previousCatalog);
|
|
180
|
+
const previous = validateCatalog(JSON.parse(await fs.readFile(file, "utf8")));
|
|
181
|
+
const replacements = new Map(catalog.models.map((model) => [model.slug, model]));
|
|
182
|
+
// Preserve foreign entries. Active model entries must keep their
|
|
183
|
+
// official instructions rather than inheriting arbitrary user prompts.
|
|
184
|
+
catalog = { ...previous, models: previous.models.map((model) =>
|
|
185
|
+
artifacts.catalogPlan.models.includes(model.slug) ? replacements.get(model.slug) : model,
|
|
186
|
+
) };
|
|
187
|
+
const present = new Set(catalog.models.map((model) => model.slug));
|
|
188
|
+
for (const model of replacements.values()) if (!present.has(model.slug)) catalog.models.push(model);
|
|
189
|
+
warnings.push("已保留原模型目录的其他条目;当前模型的覆盖在 Codex 配置中是全局的,不按 provider 隔离。");
|
|
190
|
+
}
|
|
191
|
+
const contents = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
192
|
+
const digest = createHash("sha256").update(contents).digest("hex").slice(0,24);
|
|
193
|
+
const relative = `model-catalogs/cabletidy-${digest}.json`;
|
|
194
|
+
rootValues.model_catalog_json = path.join(codexHome, relative);
|
|
195
|
+
rootValues.model_context_window = undefined;
|
|
196
|
+
catalogState = {
|
|
197
|
+
version: 1,
|
|
198
|
+
sourceVersion: artifacts.catalogPlan.sourceVersion,
|
|
199
|
+
managedPath: rootValues.model_catalog_json,
|
|
200
|
+
previousCatalog,
|
|
201
|
+
previousContext,
|
|
202
|
+
};
|
|
203
|
+
files.push({ path: relative, contents, kind: "json" });
|
|
204
|
+
if (current.model_context_window !== undefined) warnings.push("将暂时移除根级 model_context_window,使逐模型窗口生效;返回官方默认模式时恢复。");
|
|
205
|
+
} else if (ownsCatalog) {
|
|
206
|
+
rootValues.model_catalog_json = previousCatalog;
|
|
207
|
+
// Do not discard a root override the user added after our last apply.
|
|
208
|
+
if (current.model_context_window === undefined) rootValues.model_context_window = previousContext;
|
|
209
|
+
catalogState = null;
|
|
210
|
+
}
|
|
211
|
+
for (const key of ["model_instructions_file", "model_auto_compact_token_limit", "model_supports_reasoning_summaries"]) {
|
|
212
|
+
if (current[key] !== undefined) warnings.push(`保留现有 ${key},该用户配置仍会影响 Codex 行为。`);
|
|
213
|
+
}
|
|
214
|
+
if (!artifacts.catalogPlan?.catalog && previousCatalog) warnings.push("保留现有 model_catalog_json;Codex 将继续使用用户的模型目录。");
|
|
215
|
+
if (!artifacts.catalogPlan?.catalog && previousContext !== undefined) warnings.push("保留现有 model_context_window,它会覆盖模型目录中的上下文窗口。");
|
|
216
|
+
let root = patchRootConfig(existing, rootValues);
|
|
217
|
+
root = patchProviderConfig(root, artifacts.providerId, artifacts.providerContents);
|
|
218
|
+
return {
|
|
219
|
+
...artifacts,
|
|
220
|
+
codexHome,
|
|
221
|
+
rootBefore: existing,
|
|
222
|
+
catalogState,
|
|
223
|
+
warnings: [...new Set(warnings)],
|
|
224
|
+
catalogSummary: artifacts.catalogPlan ? {
|
|
225
|
+
sourceVersion: artifacts.catalogPlan.sourceVersion,
|
|
226
|
+
mode: artifacts.catalogPlan.catalog ? "managed" : "official",
|
|
227
|
+
overrides: artifacts.catalogPlan.overrides,
|
|
228
|
+
} : undefined,
|
|
229
|
+
files: [{ path: "config.toml", contents: `${root.trimEnd()}\n` }, ...files],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function safeRelativePath(value) {
|
|
234
|
+
const normalized = String(value || "").replaceAll("\\", "/");
|
|
235
|
+
if (
|
|
236
|
+
!normalized ||
|
|
237
|
+
normalized === "." ||
|
|
238
|
+
normalized.startsWith("/") ||
|
|
239
|
+
/^[a-zA-Z]:\//.test(normalized) ||
|
|
240
|
+
normalized.includes("\0") ||
|
|
241
|
+
normalized.split("/").includes("..")
|
|
242
|
+
) {
|
|
243
|
+
throw new Error(`Codex artifact 文件路径不安全: ${value}`);
|
|
244
|
+
}
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function writeArtifact(file, contents, paths, label) {
|
|
249
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
250
|
+
await backupFile(file, paths.backups, label);
|
|
251
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
252
|
+
await fs.writeFile(temporary, contents, { mode: 0o600 });
|
|
253
|
+
await fs.rename(temporary, file);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let applyOperation = Promise.resolve();
|
|
257
|
+
|
|
258
|
+
export function applyCodexArtifacts(artifacts, options = {}) {
|
|
259
|
+
const operation = applyOperation.then(() => applyCodexArtifactsNow(artifacts, options));
|
|
260
|
+
applyOperation = operation.catch(() => {});
|
|
261
|
+
return operation;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function applyCodexArtifactsNow(artifacts, options = {}) {
|
|
265
|
+
if (!artifacts?.activeContents || !artifacts.providerContents || !artifacts.providerId) {
|
|
266
|
+
throw new Error("Unsupported Codex artifacts: expected a generated provider configuration");
|
|
267
|
+
}
|
|
268
|
+
const staged = await stageCodexArtifacts(artifacts, options);
|
|
269
|
+
const paths = options.paths || { backups: path.join(os.homedir(), ".cabletidy", "backups") };
|
|
270
|
+
await fs.mkdir(paths.backups, { recursive: true, mode: 0o700 });
|
|
271
|
+
await fs.mkdir(staged.codexHome, { recursive: true, mode: 0o700 });
|
|
272
|
+
const rootFile = path.join(staged.codexHome, "config.toml");
|
|
273
|
+
const stateFile = path.join(staged.codexHome, CATALOG_STATE_FILE);
|
|
274
|
+
const previousState = await readOptional(stateFile);
|
|
275
|
+
const applied = [];
|
|
276
|
+
// Content-addressed catalogs are written first, so a failed config write
|
|
277
|
+
// never changes the catalog used by the previous configuration.
|
|
278
|
+
for (const file of staged.files.slice(1)) {
|
|
279
|
+
const target = path.join(staged.codexHome, safeRelativePath(file.path));
|
|
280
|
+
await writeArtifact(target, file.contents, paths, "codex-model-catalog.json");
|
|
281
|
+
applied.push(target);
|
|
282
|
+
}
|
|
283
|
+
if (await readOptional(rootFile) !== staged.rootBefore) {
|
|
284
|
+
throw new Error("Codex 配置在应用期间已改变,请重新预览并重试");
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
if (staged.catalogState !== undefined) {
|
|
288
|
+
await writeArtifact(stateFile, `${JSON.stringify(staged.catalogState)}\n`, paths, "codex-catalog-state.json");
|
|
289
|
+
}
|
|
290
|
+
await writeArtifact(rootFile, staged.files[0].contents, paths, "codex-config.toml");
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (staged.catalogState !== undefined) {
|
|
293
|
+
if (previousState) await fs.writeFile(stateFile, previousState, { mode: 0o600 });
|
|
294
|
+
else await fs.unlink(stateFile).catch(() => {});
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
applied.unshift(rootFile);
|
|
299
|
+
return { applied, environment: staged.environment, mode: staged.mode, warnings: staged.warnings, catalogSummary: staged.catalogSummary };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function publicArtifacts(artifacts) {
|
|
303
|
+
const { catalogPlan, catalogState, rootBefore, codexHome, ...publicValues } = artifacts;
|
|
304
|
+
return {
|
|
305
|
+
...publicValues,
|
|
306
|
+
environment: {
|
|
307
|
+
vars: artifacts.environment?.vars || {},
|
|
308
|
+
shell: artifacts.environment?.shell || "",
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|