codex-configurator 0.2.4 → 0.2.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.
@@ -0,0 +1,282 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import {
5
+ getReferenceSchemaRevision,
6
+ setReferenceSchema,
7
+ } from './configReference.js';
8
+ import { logConfiguratorError } from './errorLogger.js';
9
+
10
+ const CONFIG_SCHEMA_URL = 'https://developers.openai.com/codex/config-schema.json';
11
+ const SCHEMA_CACHE_DIRECTORY_NAME = 'codex-configurator-cache';
12
+ const SCHEMA_CACHE_FILE_NAME = 'config-schema.json';
13
+ const SCHEMA_METADATA_FILE_NAME = 'config-schema.meta.json';
14
+ const FETCH_TIMEOUT_MS = 10000;
15
+ const MAX_SCHEMA_BYTES = 512 * 1024;
16
+
17
+ const isObject = (value) =>
18
+ value !== null && typeof value === 'object' && !Array.isArray(value);
19
+
20
+ const normalizeText = (value) => String(value || '').trim();
21
+
22
+ const resolveCodexDirectory = ({ mainConfigPath = '', homeDir = os.homedir() } = {}) => {
23
+ const normalizedPath = normalizeText(mainConfigPath);
24
+ if (!normalizedPath) {
25
+ return path.join(homeDir, '.codex');
26
+ }
27
+
28
+ return path.dirname(path.resolve(normalizedPath));
29
+ };
30
+
31
+ export const resolveSchemaCachePaths = ({ mainConfigPath = '', homeDir = os.homedir() } = {}) => {
32
+ const codexDirectory = resolveCodexDirectory({ mainConfigPath, homeDir });
33
+ const cacheDirectory = path.join(codexDirectory, SCHEMA_CACHE_DIRECTORY_NAME);
34
+ return {
35
+ codexDirectory,
36
+ cacheDirectory,
37
+ schemaPath: path.join(cacheDirectory, SCHEMA_CACHE_FILE_NAME),
38
+ metadataPath: path.join(cacheDirectory, SCHEMA_METADATA_FILE_NAME),
39
+ };
40
+ };
41
+
42
+ const readJsonFile = (targetPath) => {
43
+ try {
44
+ const payload = fs.readFileSync(targetPath, 'utf8');
45
+ return JSON.parse(payload);
46
+ } catch {
47
+ return null;
48
+ }
49
+ };
50
+
51
+ const writeJsonFileAtomic = (targetPath, payload) => {
52
+ const directoryPath = path.dirname(targetPath);
53
+ const fileName = path.basename(targetPath);
54
+ const tempPath = path.join(
55
+ directoryPath,
56
+ `.${fileName}.${process.pid}.${Date.now()}.tmp`
57
+ );
58
+
59
+ if (!fs.existsSync(directoryPath)) {
60
+ fs.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
61
+ }
62
+
63
+ let fileDescriptor = null;
64
+
65
+ try {
66
+ fileDescriptor = fs.openSync(tempPath, 'wx', 0o600);
67
+ fs.writeFileSync(fileDescriptor, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
68
+ fs.fsyncSync(fileDescriptor);
69
+ fs.closeSync(fileDescriptor);
70
+ fileDescriptor = null;
71
+ fs.renameSync(tempPath, targetPath);
72
+ } finally {
73
+ if (fileDescriptor !== null) {
74
+ try {
75
+ fs.closeSync(fileDescriptor);
76
+ } catch {}
77
+ }
78
+
79
+ if (fs.existsSync(tempPath)) {
80
+ try {
81
+ fs.unlinkSync(tempPath);
82
+ } catch {}
83
+ }
84
+ }
85
+ };
86
+
87
+ const applySchema = (schema) => {
88
+ const previousRevision = getReferenceSchemaRevision();
89
+ const ok = setReferenceSchema(schema);
90
+ if (!ok) {
91
+ return {
92
+ ok: false,
93
+ changed: false,
94
+ };
95
+ }
96
+
97
+ return {
98
+ ok: true,
99
+ changed: previousRevision !== getReferenceSchemaRevision(),
100
+ };
101
+ };
102
+
103
+ const buildConditionalHeaders = (metadata = {}) => {
104
+ const headers = {
105
+ accept: 'application/json',
106
+ };
107
+ const etag = normalizeText(metadata.etag);
108
+ const lastModified = normalizeText(metadata.lastModified);
109
+
110
+ if (etag) {
111
+ headers['if-none-match'] = etag;
112
+ }
113
+
114
+ if (lastModified) {
115
+ headers['if-modified-since'] = lastModified;
116
+ }
117
+
118
+ return headers;
119
+ };
120
+
121
+ const fetchRemoteSchema = async ({
122
+ fetchImpl = fetch,
123
+ metadata = {},
124
+ }) => {
125
+ const controller = new AbortController();
126
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
127
+
128
+ try {
129
+ const response = await fetchImpl(CONFIG_SCHEMA_URL, {
130
+ headers: buildConditionalHeaders(metadata),
131
+ signal: controller.signal,
132
+ });
133
+
134
+ if (response.status === 304) {
135
+ return {
136
+ status: 'not-modified',
137
+ };
138
+ }
139
+
140
+ if (!response.ok) {
141
+ throw new Error(
142
+ `Schema request failed (${response.status} ${response.statusText})`
143
+ );
144
+ }
145
+
146
+ const contentType = normalizeText(response.headers.get('content-type')).toLowerCase();
147
+ if (!contentType.includes('application/json')) {
148
+ throw new Error('Schema response content-type is not application/json.');
149
+ }
150
+
151
+ const bytes = Buffer.from(await response.arrayBuffer());
152
+ if (bytes.length > MAX_SCHEMA_BYTES) {
153
+ throw new Error(
154
+ `Schema response exceeded max size (${bytes.length} bytes > ${MAX_SCHEMA_BYTES} bytes).`
155
+ );
156
+ }
157
+
158
+ let parsedSchema = null;
159
+ try {
160
+ parsedSchema = JSON.parse(bytes.toString('utf8'));
161
+ } catch (error) {
162
+ throw new Error(
163
+ `Schema response was not valid JSON (${String(error?.message || 'parse failed')}).`
164
+ );
165
+ }
166
+
167
+ return {
168
+ status: 'updated',
169
+ schema: parsedSchema,
170
+ metadata: {
171
+ etag: normalizeText(response.headers.get('etag')) || null,
172
+ lastModified: normalizeText(response.headers.get('last-modified')) || null,
173
+ },
174
+ };
175
+ } finally {
176
+ clearTimeout(timeoutId);
177
+ }
178
+ };
179
+
180
+ export const syncReferenceSchemaAtStartup = async ({
181
+ mainConfigPath = '',
182
+ fetchImpl = fetch,
183
+ onSchemaChange,
184
+ onStatus,
185
+ } = {}) => {
186
+ const cachePaths = resolveSchemaCachePaths({ mainConfigPath });
187
+ const notifySchemaChange = (source) => {
188
+ if (typeof onSchemaChange === 'function') {
189
+ onSchemaChange({ source });
190
+ }
191
+ };
192
+ const updateStatus = (value) => {
193
+ if (typeof onStatus === 'function') {
194
+ onStatus(String(value || '').trim());
195
+ }
196
+ };
197
+
198
+ let source = 'bundled';
199
+ let canReuseCachedValidators = false;
200
+
201
+ try {
202
+ updateStatus('Schema: Loading cache...');
203
+
204
+ const cachedSchema = readJsonFile(cachePaths.schemaPath);
205
+ const metadataPayload = readJsonFile(cachePaths.metadataPath);
206
+ const cachedMetadata = isObject(metadataPayload) ? metadataPayload : {};
207
+
208
+ if (cachedSchema !== null) {
209
+ const cacheApplyResult = applySchema(cachedSchema);
210
+ if (cacheApplyResult.ok) {
211
+ source = 'cache';
212
+ canReuseCachedValidators = true;
213
+ if (cacheApplyResult.changed) {
214
+ notifySchemaChange('cache');
215
+ }
216
+ } else {
217
+ logConfiguratorError('schema.cache.invalid', {
218
+ schemaPath: cachePaths.schemaPath,
219
+ });
220
+ }
221
+ }
222
+
223
+ updateStatus('Schema: Checking upstream...');
224
+
225
+ const fetchResult = await fetchRemoteSchema({
226
+ fetchImpl,
227
+ metadata: canReuseCachedValidators ? cachedMetadata : {},
228
+ });
229
+
230
+ if (fetchResult.status === 'not-modified') {
231
+ return {
232
+ ok: true,
233
+ updated: false,
234
+ source,
235
+ };
236
+ }
237
+
238
+ const remoteApplyResult = applySchema(fetchResult.schema);
239
+ if (!remoteApplyResult.ok) {
240
+ throw new Error('Downloaded schema did not pass local validation.');
241
+ }
242
+ if (remoteApplyResult.changed) {
243
+ notifySchemaChange('remote');
244
+ }
245
+
246
+ updateStatus('Schema: Saving cache...');
247
+
248
+ const mergedMetadata = {
249
+ etag: fetchResult.metadata.etag || normalizeText(cachedMetadata.etag) || null,
250
+ lastModified:
251
+ fetchResult.metadata.lastModified ||
252
+ normalizeText(cachedMetadata.lastModified) ||
253
+ null,
254
+ updatedAt: new Date().toISOString(),
255
+ sourceUrl: CONFIG_SCHEMA_URL,
256
+ };
257
+
258
+ writeJsonFileAtomic(cachePaths.schemaPath, fetchResult.schema);
259
+ writeJsonFileAtomic(cachePaths.metadataPath, mergedMetadata);
260
+
261
+ return {
262
+ ok: true,
263
+ updated: true,
264
+ source: 'remote',
265
+ };
266
+ } catch (error) {
267
+ const message = String(error?.message || 'Unknown schema sync error.');
268
+ logConfiguratorError('schema.sync.failed', {
269
+ error: message,
270
+ source,
271
+ schemaPath: cachePaths.schemaPath,
272
+ metadataPath: cachePaths.metadataPath,
273
+ });
274
+
275
+ return {
276
+ ok: false,
277
+ updated: false,
278
+ source,
279
+ error: message,
280
+ };
281
+ }
282
+ };