codex-configurator 0.2.5 → 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.
- package/README.md +13 -1
- package/index.js +73 -4
- package/package.json +1 -1
- package/src/appState.js +11 -0
- package/src/components/Header.js +0 -5
- package/src/configFeatures.js +15 -16
- package/src/configReference.js +66 -19
- package/src/schemaRuntimeSync.js +282 -0
- package/src/ui/panes/StatusLine.js +71 -28
package/README.md
CHANGED
|
@@ -120,6 +120,8 @@ You can override either command with:
|
|
|
120
120
|
- `CODEX_CONFIGURATOR_CODEX_BIN=<command-or-path>`
|
|
121
121
|
- `CODEX_CONFIGURATOR_NPM_BIN=<command-or-path>`
|
|
122
122
|
|
|
123
|
+
The status bar shows `Using Codex v<version>` only after Codex version detection completes. During startup schema sync it shows `Checking schema`, and after that it shows `Checking Codex version` until the Codex check finishes. When needed, it shows Codex update state as `Update available` or `Version check unavailable`; if there is no warning badge, assume Codex is up to date. The schema-progress state is kept visible for at least `1.2s` so it is observable even on very fast connections. The Codex version check starts only after schema sync completes.
|
|
124
|
+
|
|
123
125
|
## Self-update behavior
|
|
124
126
|
|
|
125
127
|
On startup, the app checks the npm registry for the latest `codex-configurator` version.
|
|
@@ -132,7 +134,17 @@ This uses the `npm` command from `PATH` (or `CODEX_CONFIGURATOR_NPM_BIN` if set)
|
|
|
132
134
|
## Upstream reference
|
|
133
135
|
|
|
134
136
|
- Canonical config schema: https://developers.openai.com/codex/config-schema.json
|
|
135
|
-
-
|
|
137
|
+
- Runtime behavior:
|
|
138
|
+
- App starts with bundled fallback schema from `src/reference/config-schema.json`.
|
|
139
|
+
- App then checks for updates on startup in the background.
|
|
140
|
+
- If a valid cached schema exists, it is loaded immediately.
|
|
141
|
+
- If upstream has a newer valid schema, it is applied immediately and cached for future launches.
|
|
142
|
+
- If the check fails, the app silently continues with the already-active schema.
|
|
143
|
+
- Runtime cache location: `<codex-dir>/codex-configurator-cache/`
|
|
144
|
+
- Default codex dir: `~/.codex`
|
|
145
|
+
- Cached files:
|
|
146
|
+
- `config-schema.json`
|
|
147
|
+
- `config-schema.meta.json`
|
|
136
148
|
- To refresh the reference snapshot after a new stable Codex release:
|
|
137
149
|
- `npm run sync:reference`
|
|
138
150
|
- Snapshot currently synced against Codex stable reference updates published on 2026-02-25.
|
package/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import {
|
|
37
37
|
APP_MODES,
|
|
38
38
|
APP_STATE_ACTION,
|
|
39
|
+
REFERENCE_SCHEMA_CHANGED_ACTION,
|
|
39
40
|
appStateReducer,
|
|
40
41
|
buildInitialAppState,
|
|
41
42
|
} from './src/appState.js';
|
|
@@ -43,6 +44,7 @@ import { pathToKey, clamp, computeListViewportRows } from './src/layout.js';
|
|
|
43
44
|
import { Header } from './src/components/Header.js';
|
|
44
45
|
import { ConfigNavigator } from './src/components/ConfigNavigator.js';
|
|
45
46
|
import { filterRowsByQuery } from './src/fuzzySearch.js';
|
|
47
|
+
import { syncReferenceSchemaAtStartup } from './src/schemaRuntimeSync.js';
|
|
46
48
|
import { executeInputCommand, getModeHint } from './src/ui/commands.js';
|
|
47
49
|
import { CommandBar } from './src/ui/panes/CommandBar.js';
|
|
48
50
|
import { HelpBubble } from './src/ui/panes/HelpBubble.js';
|
|
@@ -75,6 +77,7 @@ const UPDATE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
|
75
77
|
const CODEX_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_CODEX_BIN';
|
|
76
78
|
const NPM_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_NPM_BIN';
|
|
77
79
|
const CONFIGURATOR_PACKAGE_NAME = 'codex-configurator';
|
|
80
|
+
const SCHEMA_STATUS_MIN_VISIBLE_MS = 1200;
|
|
78
81
|
const FILE_SWITCH_MAX_VISIBLE_ENTRIES = 6;
|
|
79
82
|
const FILE_SWITCH_PANEL_BASE_ROWS = 3;
|
|
80
83
|
const FILE_SWITCH_LAYOUT_EXTRA_GAP_ROWS = 1;
|
|
@@ -174,6 +177,11 @@ const runCommand = async (command, args = [], options = {}) => {
|
|
|
174
177
|
return result.stdout;
|
|
175
178
|
};
|
|
176
179
|
|
|
180
|
+
const waitForMilliseconds = (milliseconds) =>
|
|
181
|
+
new Promise((resolve) => {
|
|
182
|
+
setTimeout(resolve, Math.max(0, Number(milliseconds) || 0));
|
|
183
|
+
});
|
|
184
|
+
|
|
177
185
|
const getConfiguredCommand = (environmentVariableName, fallbackCommand) => {
|
|
178
186
|
const configuredCommand = String(
|
|
179
187
|
process.env[environmentVariableName] || '',
|
|
@@ -364,7 +372,7 @@ const getCodexUpdateStatus = async () => {
|
|
|
364
372
|
return {
|
|
365
373
|
installed: installedLabel,
|
|
366
374
|
latest: 'unknown',
|
|
367
|
-
status: '
|
|
375
|
+
status: 'Version check unavailable',
|
|
368
376
|
};
|
|
369
377
|
}
|
|
370
378
|
|
|
@@ -380,7 +388,7 @@ const getCodexUpdateStatus = async () => {
|
|
|
380
388
|
return {
|
|
381
389
|
installed,
|
|
382
390
|
latest: 'unknown',
|
|
383
|
-
status: '
|
|
391
|
+
status: 'Version check unavailable',
|
|
384
392
|
};
|
|
385
393
|
}
|
|
386
394
|
|
|
@@ -389,14 +397,14 @@ const getCodexUpdateStatus = async () => {
|
|
|
389
397
|
return {
|
|
390
398
|
installed,
|
|
391
399
|
latest,
|
|
392
|
-
status:
|
|
400
|
+
status: 'Update available',
|
|
393
401
|
};
|
|
394
402
|
}
|
|
395
403
|
|
|
396
404
|
return {
|
|
397
405
|
installed,
|
|
398
406
|
latest,
|
|
399
|
-
status: '
|
|
407
|
+
status: 'Up to date',
|
|
400
408
|
};
|
|
401
409
|
};
|
|
402
410
|
|
|
@@ -470,6 +478,8 @@ const App = () => {
|
|
|
470
478
|
});
|
|
471
479
|
const setStateBatch = (updates) =>
|
|
472
480
|
dispatch({ type: APP_STATE_ACTION, payload: { updates } });
|
|
481
|
+
const resetSchemaDerivedEditState = () =>
|
|
482
|
+
dispatch({ type: REFERENCE_SCHEMA_CHANGED_ACTION });
|
|
473
483
|
|
|
474
484
|
const setSnapshot = (valueOrUpdater) =>
|
|
475
485
|
setAppState('snapshot', valueOrUpdater);
|
|
@@ -517,6 +527,10 @@ const App = () => {
|
|
|
517
527
|
setAppState('codexVersion', valueOrUpdater);
|
|
518
528
|
const setCodexVersionStatus = (valueOrUpdater) =>
|
|
519
529
|
setAppState('codexVersionStatus', valueOrUpdater);
|
|
530
|
+
const setIsSchemaCheckInProgress = (valueOrUpdater) =>
|
|
531
|
+
setAppState('isSchemaCheckInProgress', valueOrUpdater);
|
|
532
|
+
const setSchemaCheckStatusText = (valueOrUpdater) =>
|
|
533
|
+
setAppState('schemaCheckStatusText', valueOrUpdater);
|
|
520
534
|
const {
|
|
521
535
|
snapshot,
|
|
522
536
|
snapshotByFileId,
|
|
@@ -538,6 +552,8 @@ const App = () => {
|
|
|
538
552
|
showHelp,
|
|
539
553
|
codexVersion,
|
|
540
554
|
codexVersionStatus,
|
|
555
|
+
isSchemaCheckInProgress,
|
|
556
|
+
schemaCheckStatusText,
|
|
541
557
|
} = state;
|
|
542
558
|
commandInputRef.current = String(commandInput || '');
|
|
543
559
|
const { exit } = useApp();
|
|
@@ -552,6 +568,10 @@ const App = () => {
|
|
|
552
568
|
: APP_MODES.BROWSE;
|
|
553
569
|
|
|
554
570
|
useEffect(() => {
|
|
571
|
+
if (isSchemaCheckInProgress) {
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
|
|
555
575
|
let isCancelled = false;
|
|
556
576
|
|
|
557
577
|
const loadVersionStatus = async () => {
|
|
@@ -570,9 +590,56 @@ const App = () => {
|
|
|
570
590
|
await ensureLatestConfiguratorVersion(commands.npmCommand);
|
|
571
591
|
};
|
|
572
592
|
|
|
593
|
+
setCodexVersionStatus('Checking Codex version');
|
|
573
594
|
loadVersionStatus();
|
|
574
595
|
ensureLatestConfigurator();
|
|
575
596
|
|
|
597
|
+
return () => {
|
|
598
|
+
isCancelled = true;
|
|
599
|
+
};
|
|
600
|
+
}, [isSchemaCheckInProgress]);
|
|
601
|
+
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
let isCancelled = false;
|
|
604
|
+
|
|
605
|
+
const syncSchemaReference = async () => {
|
|
606
|
+
const checkStartedAt = Date.now();
|
|
607
|
+
const updateSchemaStatus = (status) => {
|
|
608
|
+
if (isCancelled) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
setSchemaCheckStatusText(status);
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
await syncReferenceSchemaAtStartup({
|
|
616
|
+
mainConfigPath: initialMainSnapshot.path,
|
|
617
|
+
onSchemaChange: () => {
|
|
618
|
+
if (isCancelled) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
resetSchemaDerivedEditState();
|
|
623
|
+
},
|
|
624
|
+
onStatus: updateSchemaStatus,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const elapsed = Date.now() - checkStartedAt;
|
|
628
|
+
const remainingVisibleMs = SCHEMA_STATUS_MIN_VISIBLE_MS - elapsed;
|
|
629
|
+
if (remainingVisibleMs > 0) {
|
|
630
|
+
await waitForMilliseconds(remainingVisibleMs);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (isCancelled) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
setSchemaCheckStatusText('');
|
|
638
|
+
setIsSchemaCheckInProgress(false);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
syncSchemaReference();
|
|
642
|
+
|
|
576
643
|
return () => {
|
|
577
644
|
isCancelled = true;
|
|
578
645
|
};
|
|
@@ -1447,6 +1514,8 @@ const App = () => {
|
|
|
1447
1514
|
React.createElement(StatusLine, {
|
|
1448
1515
|
codexVersion,
|
|
1449
1516
|
codexVersionStatus,
|
|
1517
|
+
isSchemaCheckInProgress,
|
|
1518
|
+
schemaCheckStatusText,
|
|
1450
1519
|
activeConfigFile,
|
|
1451
1520
|
appMode,
|
|
1452
1521
|
}),
|
package/package.json
CHANGED
package/src/appState.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const APP_STATE_ACTION = 'APP_STATE_ACTION';
|
|
2
|
+
export const REFERENCE_SCHEMA_CHANGED_ACTION = 'REFERENCE_SCHEMA_CHANGED_ACTION';
|
|
2
3
|
|
|
3
4
|
export const APP_MODES = {
|
|
4
5
|
BROWSE: 'browse',
|
|
@@ -9,6 +10,14 @@ export const APP_MODES = {
|
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
export const appStateReducer = (state, action) => {
|
|
13
|
+
if (action.type === REFERENCE_SCHEMA_CHANGED_ACTION) {
|
|
14
|
+
return {
|
|
15
|
+
...state,
|
|
16
|
+
editMode: null,
|
|
17
|
+
editError: '',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
if (action.type !== APP_STATE_ACTION) {
|
|
13
22
|
return state;
|
|
14
23
|
}
|
|
@@ -66,4 +75,6 @@ export const buildInitialAppState = (initialMainSnapshot, initialCatalog, initia
|
|
|
66
75
|
showHelp: false,
|
|
67
76
|
codexVersion: 'version loading...',
|
|
68
77
|
codexVersionStatus: '',
|
|
78
|
+
isSchemaCheckInProgress: true,
|
|
79
|
+
schemaCheckStatusText: 'Checking schema',
|
|
69
80
|
});
|
package/src/components/Header.js
CHANGED
package/src/configFeatures.js
CHANGED
|
@@ -35,25 +35,24 @@ const buildFeatureDefinitionFromReference = (key) => {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return acc;
|
|
48
|
-
}, {});
|
|
38
|
+
const buildFeatureDefinitionMap = () =>
|
|
39
|
+
getReferenceFeatureKeys()
|
|
40
|
+
.map((key) => buildFeatureDefinitionFromReference(key))
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.reduce((acc, definition) => {
|
|
43
|
+
acc[definition.key] = definition;
|
|
44
|
+
return acc;
|
|
45
|
+
}, {});
|
|
49
46
|
|
|
50
47
|
export const getConfigFeatureKeys = () => {
|
|
51
|
-
return
|
|
48
|
+
return getReferenceFeatureKeys();
|
|
52
49
|
};
|
|
53
50
|
|
|
54
|
-
export const getConfigFeatureDefinition = (key) =>
|
|
51
|
+
export const getConfigFeatureDefinition = (key) =>
|
|
52
|
+
buildFeatureDefinitionMap()[String(key || '').trim()];
|
|
55
53
|
|
|
56
54
|
export const getConfigFeatureDefinitionOrFallback = (key) => {
|
|
55
|
+
const normalizedKey = String(key || '').trim();
|
|
57
56
|
if (!key) {
|
|
58
57
|
return {
|
|
59
58
|
short: `${prettifyFeatureName(String(key))}`,
|
|
@@ -64,9 +63,9 @@ export const getConfigFeatureDefinitionOrFallback = (key) => {
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
return (
|
|
67
|
-
|
|
68
|
-
key,
|
|
69
|
-
short: prettifyFeatureName(
|
|
66
|
+
getConfigFeatureDefinition(normalizedKey) || {
|
|
67
|
+
key: normalizedKey,
|
|
68
|
+
short: prettifyFeatureName(normalizedKey),
|
|
70
69
|
usage: 'This configured key is not in the official feature list.',
|
|
71
70
|
defaultValue: false,
|
|
72
71
|
isDocumented: false,
|
package/src/configReference.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
|
-
const
|
|
4
|
+
const BUNDLED_CONFIG_SCHEMA_DATA = require('./reference/config-schema.json');
|
|
5
5
|
|
|
6
6
|
const ROOT_PLACEHOLDER = '<name>';
|
|
7
7
|
const PLACEHOLDER_SEGMENT = /^<[^>]+>$/;
|
|
@@ -38,6 +38,8 @@ const normalizeType = (type) => String(type || '').replace(/\s+/g, ' ').trim();
|
|
|
38
38
|
const isObject = (value) =>
|
|
39
39
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
40
40
|
|
|
41
|
+
const buildSchemaRevision = (schema) => JSON.stringify(schema);
|
|
42
|
+
|
|
41
43
|
const isPrimitiveValue = (type) =>
|
|
42
44
|
type === 'string' || type === 'number' || type === 'integer' || type === 'boolean';
|
|
43
45
|
|
|
@@ -966,9 +968,62 @@ const fullPathMatches = (referencePath, actualPath) =>
|
|
|
966
968
|
const countConcreteSegments = (segments) =>
|
|
967
969
|
segments.reduce((count, segment) => count + (isPlaceholderSegment(segment) ? 0 : 1), 0);
|
|
968
970
|
|
|
969
|
-
const
|
|
971
|
+
const buildFeatureKeys = (referenceOptions) => [
|
|
972
|
+
...new Set(
|
|
973
|
+
referenceOptions
|
|
974
|
+
.filter(
|
|
975
|
+
(option) =>
|
|
976
|
+
option.keyPath.length === 2 &&
|
|
977
|
+
option.keyPath[0] === 'features' &&
|
|
978
|
+
!isPlaceholderSegment(option.keyPath[1])
|
|
979
|
+
)
|
|
980
|
+
.map((option) => option.keyPath[1])
|
|
981
|
+
),
|
|
982
|
+
].sort((left, right) => left.localeCompare(right));
|
|
983
|
+
|
|
984
|
+
const prepareReferenceState = (schema) => {
|
|
985
|
+
if (!isObject(schema)) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const referenceOptions = buildReferenceOptions(schema);
|
|
990
|
+
if (!Array.isArray(referenceOptions) || referenceOptions.length === 0) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return {
|
|
995
|
+
schema,
|
|
996
|
+
revision: buildSchemaRevision(schema),
|
|
997
|
+
referenceOptions,
|
|
998
|
+
optionsByKey: new Map(referenceOptions.map((option) => [option.key, option])),
|
|
999
|
+
featureKeys: buildFeatureKeys(referenceOptions),
|
|
1000
|
+
};
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
let activeReferenceState = prepareReferenceState(BUNDLED_CONFIG_SCHEMA_DATA);
|
|
1004
|
+
if (!activeReferenceState) {
|
|
1005
|
+
throw new Error('Bundled config schema is invalid.');
|
|
1006
|
+
}
|
|
970
1007
|
|
|
971
|
-
const
|
|
1008
|
+
const getReferenceState = () => activeReferenceState;
|
|
1009
|
+
|
|
1010
|
+
export const isReferenceSchemaValid = (schema) => Boolean(prepareReferenceState(schema));
|
|
1011
|
+
|
|
1012
|
+
export const getReferenceSchemaRevision = () => getReferenceState().revision;
|
|
1013
|
+
|
|
1014
|
+
export const setReferenceSchema = (schema) => {
|
|
1015
|
+
const nextState = prepareReferenceState(schema);
|
|
1016
|
+
if (!nextState) {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (activeReferenceState?.revision === nextState.revision) {
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
activeReferenceState = nextState;
|
|
1025
|
+
return true;
|
|
1026
|
+
};
|
|
972
1027
|
|
|
973
1028
|
const mergeDefinition = (map, definition) => {
|
|
974
1029
|
const existing = map.get(definition.key);
|
|
@@ -988,6 +1043,7 @@ const mergeDefinition = (map, definition) => {
|
|
|
988
1043
|
};
|
|
989
1044
|
|
|
990
1045
|
export const getReferenceOptionForPath = (pathSegments) => {
|
|
1046
|
+
const { optionsByKey, referenceOptions } = getReferenceState();
|
|
991
1047
|
const normalizedPath = normalizeSegments(pathSegments);
|
|
992
1048
|
if (normalizedPath.length === 0) {
|
|
993
1049
|
return null;
|
|
@@ -1016,7 +1072,7 @@ export const getReferenceOptionForPath = (pathSegments) => {
|
|
|
1016
1072
|
return candidates[0];
|
|
1017
1073
|
};
|
|
1018
1074
|
|
|
1019
|
-
const getVariantObjectPaths = (normalizedPath) => {
|
|
1075
|
+
const getVariantObjectPaths = (normalizedPath, referenceOptions) => {
|
|
1020
1076
|
const depth = normalizedPath.length;
|
|
1021
1077
|
const paths = referenceOptions
|
|
1022
1078
|
.filter((option) => pathPrefixMatches(option.keyPath, normalizedPath))
|
|
@@ -1028,6 +1084,7 @@ const getVariantObjectPaths = (normalizedPath) => {
|
|
|
1028
1084
|
};
|
|
1029
1085
|
|
|
1030
1086
|
export const getReferenceVariantForPath = (pathSegments = []) => {
|
|
1087
|
+
const { referenceOptions } = getReferenceState();
|
|
1031
1088
|
const normalizedPath = normalizeSegments(pathSegments);
|
|
1032
1089
|
if (normalizedPath.length === 0) {
|
|
1033
1090
|
return null;
|
|
@@ -1061,11 +1118,12 @@ export const getReferenceVariantForPath = (pathSegments = []) => {
|
|
|
1061
1118
|
: {},
|
|
1062
1119
|
}))
|
|
1063
1120
|
: [],
|
|
1064
|
-
objectSchemaPaths: getVariantObjectPaths(normalizedPath),
|
|
1121
|
+
objectSchemaPaths: getVariantObjectPaths(normalizedPath, referenceOptions),
|
|
1065
1122
|
};
|
|
1066
1123
|
};
|
|
1067
1124
|
|
|
1068
1125
|
export const getReferenceTableDefinitions = (pathSegments = []) => {
|
|
1126
|
+
const { referenceOptions } = getReferenceState();
|
|
1069
1127
|
const normalizedPath = normalizeSegments(pathSegments);
|
|
1070
1128
|
const childDefinitions = new Map();
|
|
1071
1129
|
const depth = normalizedPath.length;
|
|
@@ -1099,22 +1157,10 @@ export const getReferenceTableDefinitions = (pathSegments = []) => {
|
|
|
1099
1157
|
|
|
1100
1158
|
export const getReferenceRootDefinitions = () => getReferenceTableDefinitions([]);
|
|
1101
1159
|
|
|
1102
|
-
const
|
|
1103
|
-
...new Set(
|
|
1104
|
-
referenceOptions
|
|
1105
|
-
.filter(
|
|
1106
|
-
(option) =>
|
|
1107
|
-
option.keyPath.length === 2 &&
|
|
1108
|
-
option.keyPath[0] === 'features' &&
|
|
1109
|
-
!isPlaceholderSegment(option.keyPath[1])
|
|
1110
|
-
)
|
|
1111
|
-
.map((option) => option.keyPath[1])
|
|
1112
|
-
),
|
|
1113
|
-
].sort((left, right) => left.localeCompare(right));
|
|
1114
|
-
|
|
1115
|
-
export const getReferenceFeatureKeys = () => featureKeys;
|
|
1160
|
+
export const getReferenceFeatureKeys = () => [...getReferenceState().featureKeys];
|
|
1116
1161
|
|
|
1117
1162
|
export const getReferenceCustomIdPlaceholder = (pathSegments = []) => {
|
|
1163
|
+
const { referenceOptions } = getReferenceState();
|
|
1118
1164
|
const normalizedPath = normalizeSegments(pathSegments);
|
|
1119
1165
|
const depth = normalizedPath.length;
|
|
1120
1166
|
const placeholders = new Set();
|
|
@@ -1139,6 +1185,7 @@ export const getReferenceCustomIdPlaceholder = (pathSegments = []) => {
|
|
|
1139
1185
|
};
|
|
1140
1186
|
|
|
1141
1187
|
export const getReferenceDescendantOptions = (pathSegments = []) => {
|
|
1188
|
+
const { referenceOptions } = getReferenceState();
|
|
1142
1189
|
const normalizedPath = normalizeSegments(pathSegments);
|
|
1143
1190
|
|
|
1144
1191
|
return referenceOptions
|
|
@@ -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
|
+
};
|
|
@@ -2,18 +2,57 @@ import React from 'react';
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { formatActiveFileSummary } from '../../layout.js';
|
|
4
4
|
|
|
5
|
+
const SEMVER_PREFIX_PATTERN = /^\d+\.\d+\.\d+(?:[-+._][0-9A-Za-z.-]+)*$/;
|
|
6
|
+
const CHECKING_BADGE_TEXT_COLOR = 'white';
|
|
7
|
+
const CHECKING_BADGE_BACKGROUND_COLOR = 'green';
|
|
8
|
+
|
|
9
|
+
const formatCodexVersion = (version) => {
|
|
10
|
+
const versionText = String(version || '').trim();
|
|
11
|
+
if (!versionText) {
|
|
12
|
+
return '—';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return SEMVER_PREFIX_PATTERN.test(versionText)
|
|
16
|
+
? `v${versionText}`
|
|
17
|
+
: versionText;
|
|
18
|
+
};
|
|
19
|
+
|
|
5
20
|
export const StatusLine = ({
|
|
6
21
|
codexVersion,
|
|
7
22
|
codexVersionStatus,
|
|
23
|
+
isSchemaCheckInProgress,
|
|
24
|
+
schemaCheckStatusText,
|
|
8
25
|
activeConfigFile,
|
|
9
26
|
appMode,
|
|
10
27
|
}) => {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
28
|
+
const codexStatusText = codexVersionStatus || 'Checking Codex version';
|
|
29
|
+
const schemaStatus =
|
|
30
|
+
String(schemaCheckStatusText || '').trim() || 'Checking schema';
|
|
31
|
+
const statusText = codexStatusText;
|
|
32
|
+
const isUpToDate = !isSchemaCheckInProgress && statusText === 'Up to date';
|
|
33
|
+
const isCodexChecking =
|
|
34
|
+
!isSchemaCheckInProgress &&
|
|
35
|
+
String(statusText).trim() === 'Checking Codex version';
|
|
13
36
|
const isUpdateAvailable =
|
|
37
|
+
!isSchemaCheckInProgress &&
|
|
14
38
|
typeof statusText === 'string' &&
|
|
15
|
-
statusText.startsWith('
|
|
16
|
-
const
|
|
39
|
+
statusText.startsWith('Update available');
|
|
40
|
+
const shouldShowCodexStatus =
|
|
41
|
+
!isSchemaCheckInProgress && !isUpToDate && !isCodexChecking;
|
|
42
|
+
const statusPrefix = isUpdateAvailable ? '⚠️ ' : '';
|
|
43
|
+
const statusTextColor = isUpToDate ? 'white' : 'black';
|
|
44
|
+
const statusBackgroundColor = isUpToDate ? undefined : 'yellow';
|
|
45
|
+
const isCheckingLabelVisible = isSchemaCheckInProgress || isCodexChecking;
|
|
46
|
+
const versionText = isCheckingLabelVisible
|
|
47
|
+
? ` ${isSchemaCheckInProgress ? schemaStatus : codexStatusText} `
|
|
48
|
+
: `Using Codex ${formatCodexVersion(codexVersion)}`;
|
|
49
|
+
const versionTextColor = isCheckingLabelVisible
|
|
50
|
+
? CHECKING_BADGE_TEXT_COLOR
|
|
51
|
+
: 'white';
|
|
52
|
+
const versionTextBackgroundColor = isCheckingLabelVisible
|
|
53
|
+
? CHECKING_BADGE_BACKGROUND_COLOR
|
|
54
|
+
: undefined;
|
|
55
|
+
const codexStatusTextLabel = `${statusText} `;
|
|
17
56
|
const activeFileSummary = formatActiveFileSummary(activeConfigFile);
|
|
18
57
|
|
|
19
58
|
return React.createElement(
|
|
@@ -47,34 +86,38 @@ export const StatusLine = ({
|
|
|
47
86
|
{ flexDirection: 'row', gap: 2 },
|
|
48
87
|
React.createElement(
|
|
49
88
|
Text,
|
|
50
|
-
{
|
|
51
|
-
|
|
89
|
+
{
|
|
90
|
+
color: versionTextColor,
|
|
91
|
+
backgroundColor: versionTextBackgroundColor,
|
|
92
|
+
bold: isCheckingLabelVisible,
|
|
93
|
+
},
|
|
94
|
+
versionText,
|
|
52
95
|
),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
96
|
+
shouldShowCodexStatus
|
|
97
|
+
? React.createElement(
|
|
98
|
+
Box,
|
|
99
|
+
{ flexDirection: 'row' },
|
|
100
|
+
statusPrefix
|
|
101
|
+
? React.createElement(
|
|
102
|
+
Text,
|
|
103
|
+
{
|
|
104
|
+
color: statusTextColor,
|
|
105
|
+
backgroundColor: statusBackgroundColor,
|
|
106
|
+
},
|
|
107
|
+
` ${statusPrefix} `,
|
|
108
|
+
)
|
|
109
|
+
: null,
|
|
110
|
+
React.createElement(
|
|
58
111
|
Text,
|
|
59
112
|
{
|
|
60
|
-
color:
|
|
61
|
-
backgroundColor:
|
|
62
|
-
|
|
63
|
-
: 'yellow',
|
|
113
|
+
color: statusTextColor,
|
|
114
|
+
backgroundColor: statusBackgroundColor,
|
|
115
|
+
bold: true,
|
|
64
116
|
},
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Text,
|
|
70
|
-
{
|
|
71
|
-
color: isUpToDate ? 'white' : 'black',
|
|
72
|
-
backgroundColor: isUpToDate ? undefined : 'yellow',
|
|
73
|
-
bold: true,
|
|
74
|
-
},
|
|
75
|
-
`${statusText} `,
|
|
76
|
-
),
|
|
77
|
-
),
|
|
117
|
+
codexStatusTextLabel,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
: null,
|
|
78
121
|
),
|
|
79
122
|
);
|
|
80
123
|
};
|