codex-configurator 0.2.5 → 0.2.7

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 CHANGED
@@ -39,7 +39,7 @@ npm start
39
39
  - `↑` `↓` : move selection
40
40
  - `PgUp` `PgDn`: move one page up/down
41
41
  - `Home` `End`: jump to first/last item
42
- - `Enter`: open selected table; for mixed scalar/object settings, choose a preset first (object presets open nested settings); for boolean settings, toggle directly; for string settings, open inline input; for other preset values, open picker
42
+ - `Enter`: open selected table; for mixed scalar/object settings, choose a preset first (object presets open nested settings); for boolean settings, toggle directly; for string and numeric scalar settings, open inline input; for other preset values, open picker
43
43
  - `Del`: unset selected value or remove selected custom `<id>` entry from `config.toml`
44
44
  - `←` / `Backspace`: move up one level (to parent table)
45
45
  - `:`: enter command mode
@@ -49,7 +49,7 @@ npm start
49
49
  - `:help`: toggle quick help overlay
50
50
  - `:quit` (or `:q`): quit
51
51
 
52
- The right-hand pane shows what each setting means, plus a picker when a value has preset options.
52
+ The right-hand pane shows what each setting means, the current configured value for plain scalar settings, and a picker when a value has preset options.
53
53
  Deprecated settings are marked with a `[!]` warning marker; only that marker is highlighted.
54
54
  Model picker entries are curated presets maintained by this project.
55
55
  In select lists, `[default]` marks the default option.
@@ -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
- - The local menu reads directly from `src/reference/config-schema.json`, which is downloaded from the canonical schema.
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
@@ -20,11 +20,13 @@ import {
20
20
  writeConfig,
21
21
  } from './src/configParser.js';
22
22
  import { getConfigOptions, getConfigVariantMeta } from './src/configHelp.js';
23
- import {
24
- getReferenceOptionForPath,
25
- getReferenceCustomIdPlaceholder,
26
- } from './src/configReference.js';
23
+ import { getReferenceCustomIdPlaceholder } from './src/configReference.js';
27
24
  import { normalizeCustomPathId } from './src/customPathId.js';
25
+ import {
26
+ getReferenceScalarType,
27
+ getScalarEditType,
28
+ parseScalarDraftValue,
29
+ } from './src/scalarEditing.js';
28
30
  import {
29
31
  applyVariantSelection,
30
32
  buildVariantSelectorOptions,
@@ -36,6 +38,7 @@ import {
36
38
  import {
37
39
  APP_MODES,
38
40
  APP_STATE_ACTION,
41
+ REFERENCE_SCHEMA_CHANGED_ACTION,
39
42
  appStateReducer,
40
43
  buildInitialAppState,
41
44
  } from './src/appState.js';
@@ -43,6 +46,7 @@ import { pathToKey, clamp, computeListViewportRows } from './src/layout.js';
43
46
  import { Header } from './src/components/Header.js';
44
47
  import { ConfigNavigator } from './src/components/ConfigNavigator.js';
45
48
  import { filterRowsByQuery } from './src/fuzzySearch.js';
49
+ import { syncReferenceSchemaAtStartup } from './src/schemaRuntimeSync.js';
46
50
  import { executeInputCommand, getModeHint } from './src/ui/commands.js';
47
51
  import { CommandBar } from './src/ui/panes/CommandBar.js';
48
52
  import { HelpBubble } from './src/ui/panes/HelpBubble.js';
@@ -52,17 +56,6 @@ import { StatusLine } from './src/ui/panes/StatusLine.js';
52
56
  const require = createRequire(import.meta.url);
53
57
  const { version: PACKAGE_VERSION = 'unknown' } = require('./package.json');
54
58
 
55
- const isStringReferenceType = (type) =>
56
- /^string(?:\s|$)/.test(String(type || '').trim());
57
-
58
- const isStringField = (pathSegments, value) => {
59
- if (typeof value === 'string') {
60
- return true;
61
- }
62
-
63
- return isStringReferenceType(getReferenceOptionForPath(pathSegments)?.type);
64
- };
65
-
66
59
  const isCustomIdTableRow = (pathSegments, row) =>
67
60
  row?.kind === 'table' &&
68
61
  typeof row?.pathSegment === 'string' &&
@@ -75,6 +68,7 @@ const UPDATE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
75
68
  const CODEX_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_CODEX_BIN';
76
69
  const NPM_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_NPM_BIN';
77
70
  const CONFIGURATOR_PACKAGE_NAME = 'codex-configurator';
71
+ const SCHEMA_STATUS_MIN_VISIBLE_MS = 1200;
78
72
  const FILE_SWITCH_MAX_VISIBLE_ENTRIES = 6;
79
73
  const FILE_SWITCH_PANEL_BASE_ROWS = 3;
80
74
  const FILE_SWITCH_LAYOUT_EXTRA_GAP_ROWS = 1;
@@ -174,6 +168,11 @@ const runCommand = async (command, args = [], options = {}) => {
174
168
  return result.stdout;
175
169
  };
176
170
 
171
+ const waitForMilliseconds = (milliseconds) =>
172
+ new Promise((resolve) => {
173
+ setTimeout(resolve, Math.max(0, Number(milliseconds) || 0));
174
+ });
175
+
177
176
  const getConfiguredCommand = (environmentVariableName, fallbackCommand) => {
178
177
  const configuredCommand = String(
179
178
  process.env[environmentVariableName] || '',
@@ -364,7 +363,7 @@ const getCodexUpdateStatus = async () => {
364
363
  return {
365
364
  installed: installedLabel,
366
365
  latest: 'unknown',
367
- status: 'version check unavailable',
366
+ status: 'Version check unavailable',
368
367
  };
369
368
  }
370
369
 
@@ -380,7 +379,7 @@ const getCodexUpdateStatus = async () => {
380
379
  return {
381
380
  installed,
382
381
  latest: 'unknown',
383
- status: 'version check unavailable',
382
+ status: 'Version check unavailable',
384
383
  };
385
384
  }
386
385
 
@@ -389,14 +388,14 @@ const getCodexUpdateStatus = async () => {
389
388
  return {
390
389
  installed,
391
390
  latest,
392
- status: `update available: ${latest}`,
391
+ status: 'Update available',
393
392
  };
394
393
  }
395
394
 
396
395
  return {
397
396
  installed,
398
397
  latest,
399
- status: 'up to date',
398
+ status: 'Up to date',
400
399
  };
401
400
  };
402
401
 
@@ -470,6 +469,8 @@ const App = () => {
470
469
  });
471
470
  const setStateBatch = (updates) =>
472
471
  dispatch({ type: APP_STATE_ACTION, payload: { updates } });
472
+ const resetSchemaDerivedEditState = () =>
473
+ dispatch({ type: REFERENCE_SCHEMA_CHANGED_ACTION });
473
474
 
474
475
  const setSnapshot = (valueOrUpdater) =>
475
476
  setAppState('snapshot', valueOrUpdater);
@@ -517,6 +518,10 @@ const App = () => {
517
518
  setAppState('codexVersion', valueOrUpdater);
518
519
  const setCodexVersionStatus = (valueOrUpdater) =>
519
520
  setAppState('codexVersionStatus', valueOrUpdater);
521
+ const setIsSchemaCheckInProgress = (valueOrUpdater) =>
522
+ setAppState('isSchemaCheckInProgress', valueOrUpdater);
523
+ const setSchemaCheckStatusText = (valueOrUpdater) =>
524
+ setAppState('schemaCheckStatusText', valueOrUpdater);
520
525
  const {
521
526
  snapshot,
522
527
  snapshotByFileId,
@@ -538,6 +543,8 @@ const App = () => {
538
543
  showHelp,
539
544
  codexVersion,
540
545
  codexVersionStatus,
546
+ isSchemaCheckInProgress,
547
+ schemaCheckStatusText,
541
548
  } = state;
542
549
  commandInputRef.current = String(commandInput || '');
543
550
  const { exit } = useApp();
@@ -552,6 +559,10 @@ const App = () => {
552
559
  : APP_MODES.BROWSE;
553
560
 
554
561
  useEffect(() => {
562
+ if (isSchemaCheckInProgress) {
563
+ return undefined;
564
+ }
565
+
555
566
  let isCancelled = false;
556
567
 
557
568
  const loadVersionStatus = async () => {
@@ -570,9 +581,56 @@ const App = () => {
570
581
  await ensureLatestConfiguratorVersion(commands.npmCommand);
571
582
  };
572
583
 
584
+ setCodexVersionStatus('Checking Codex version');
573
585
  loadVersionStatus();
574
586
  ensureLatestConfigurator();
575
587
 
588
+ return () => {
589
+ isCancelled = true;
590
+ };
591
+ }, [isSchemaCheckInProgress]);
592
+
593
+ useEffect(() => {
594
+ let isCancelled = false;
595
+
596
+ const syncSchemaReference = async () => {
597
+ const checkStartedAt = Date.now();
598
+ const updateSchemaStatus = (status) => {
599
+ if (isCancelled) {
600
+ return;
601
+ }
602
+
603
+ setSchemaCheckStatusText(status);
604
+ };
605
+
606
+ await syncReferenceSchemaAtStartup({
607
+ mainConfigPath: initialMainSnapshot.path,
608
+ onSchemaChange: () => {
609
+ if (isCancelled) {
610
+ return;
611
+ }
612
+
613
+ resetSchemaDerivedEditState();
614
+ },
615
+ onStatus: updateSchemaStatus,
616
+ });
617
+
618
+ const elapsed = Date.now() - checkStartedAt;
619
+ const remainingVisibleMs = SCHEMA_STATUS_MIN_VISIBLE_MS - elapsed;
620
+ if (remainingVisibleMs > 0) {
621
+ await waitForMilliseconds(remainingVisibleMs);
622
+ }
623
+
624
+ if (isCancelled) {
625
+ return;
626
+ }
627
+
628
+ setSchemaCheckStatusText('');
629
+ setIsSchemaCheckInProgress(false);
630
+ };
631
+
632
+ syncSchemaReference();
633
+
576
634
  return () => {
577
635
  isCancelled = true;
578
636
  };
@@ -822,13 +880,14 @@ const App = () => {
822
880
  });
823
881
  };
824
882
 
825
- const beginTextEditing = (target, targetPath) => {
883
+ const beginScalarEditing = (target, targetPath, scalarType) => {
826
884
  setEditError('');
827
885
  setEditMode({
828
- mode: 'text',
886
+ mode: 'scalar',
887
+ scalarType,
829
888
  path: targetPath,
830
- draftValue: typeof target.value === 'string' ? target.value : '',
831
- savedValue: null,
889
+ draftValue:
890
+ typeof target.value === 'undefined' ? '' : String(target.value),
832
891
  });
833
892
  };
834
893
 
@@ -961,15 +1020,24 @@ const App = () => {
961
1020
  setEditError('');
962
1021
  };
963
1022
 
964
- const applyTextEdit = () => {
965
- if (!editMode || editMode.mode !== 'text') {
1023
+ const applyScalarEdit = () => {
1024
+ if (!editMode || editMode.mode !== 'scalar') {
1025
+ return;
1026
+ }
1027
+
1028
+ const parsedValue = parseScalarDraftValue(
1029
+ editMode.draftValue,
1030
+ editMode.scalarType,
1031
+ );
1032
+ if (!parsedValue.ok) {
1033
+ setEditError(parsedValue.error);
966
1034
  return;
967
1035
  }
968
1036
 
969
1037
  const nextData = setValueAtPath(
970
1038
  snapshot.ok ? snapshot.data : {},
971
1039
  editMode.path,
972
- editMode.draftValue,
1040
+ parsedValue.value,
973
1041
  );
974
1042
  if (!ensureAgentConfigFile(nextData, editMode.path)) {
975
1043
  return;
@@ -1062,17 +1130,13 @@ const App = () => {
1062
1130
  undefined,
1063
1131
  'value',
1064
1132
  ) || [];
1065
- if (requiredOptions.length > 0) {
1066
- return requiredOptions[0];
1067
- }
1133
+ if (requiredOptions.length > 0) {
1134
+ return requiredOptions[0];
1135
+ }
1068
1136
 
1069
- if (
1070
- isStringReferenceType(
1071
- getReferenceOptionForPath(requiredPath)?.type,
1072
- )
1073
- ) {
1074
- return '';
1075
- }
1137
+ if (getReferenceScalarType(requiredPath) === 'string') {
1138
+ return '';
1139
+ }
1076
1140
 
1077
1141
  return {};
1078
1142
  },
@@ -1273,28 +1337,28 @@ const App = () => {
1273
1337
  setCommandInput,
1274
1338
  getCommandInput: () => commandInputRef.current,
1275
1339
  setCommandMessage,
1276
- setEditError,
1277
- beginAddIdEditing,
1278
- beginTextEditing,
1279
- beginVariantEditing,
1280
- beginEditing,
1281
- beginFileSwitchMode,
1282
- applyFileSwitch,
1283
- applyTextEdit,
1284
- applyAddId,
1285
- applyVariantEdit,
1286
- applyEdit,
1340
+ setEditError,
1341
+ beginAddIdEditing,
1342
+ beginScalarEditing,
1343
+ beginVariantEditing,
1344
+ beginEditing,
1345
+ beginFileSwitchMode,
1346
+ applyFileSwitch,
1347
+ applyScalarEdit,
1348
+ applyAddId,
1349
+ applyVariantEdit,
1350
+ applyEdit,
1287
1351
  applyBooleanToggle,
1288
1352
  unsetValueAtPath,
1289
1353
  openPathView,
1290
1354
  reloadActiveConfig,
1291
1355
  getConfigOptions: getConfigOptions,
1292
- getConfigVariantMeta,
1293
- getNodeAtPath,
1294
- buildRows,
1295
- isStringField,
1296
- isCustomIdTableRow,
1297
- resolveMixedVariantBackNavigationPath,
1356
+ getConfigVariantMeta,
1357
+ getNodeAtPath,
1358
+ buildRows,
1359
+ getScalarEditType,
1360
+ isCustomIdTableRow,
1361
+ resolveMixedVariantBackNavigationPath,
1298
1362
  adjustScrollForSelection,
1299
1363
  getSavedIndex,
1300
1364
  readActiveConfigSnapshot,
@@ -1447,6 +1511,8 @@ const App = () => {
1447
1511
  React.createElement(StatusLine, {
1448
1512
  codexVersion,
1449
1513
  codexVersionStatus,
1514
+ isSchemaCheckInProgress,
1515
+ schemaCheckStatusText,
1450
1516
  activeConfigFile,
1451
1517
  appMode,
1452
1518
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-configurator",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "TOML-aware Ink TUI for Codex Configurator",
5
5
  "main": "index.js",
6
6
  "type": "module",
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
  });
@@ -10,6 +10,7 @@ import {
10
10
  import { computePaneWidths, clamp } from '../layout.js';
11
11
  import { getNodeAtPath, buildRows } from '../configParser.js';
12
12
  import { filterRowsByQuery } from '../fuzzySearch.js';
13
+ import { getReadOnlyValuePreviewText } from '../valuePreview.js';
13
14
  import {
14
15
  buildVariantSelectorOptions,
15
16
  isObjectValue,
@@ -112,7 +113,7 @@ const renderArrayDetails = (rows) => {
112
113
  );
113
114
  };
114
115
 
115
- const renderTextEditor = (draftValue) =>
116
+ const renderScalarEditor = (draftValue, scalarType) =>
116
117
  React.createElement(
117
118
  React.Fragment,
118
119
  null,
@@ -124,7 +125,13 @@ const renderTextEditor = (draftValue) =>
124
125
  React.createElement(
125
126
  Text,
126
127
  { color: 'gray', wrap: 'truncate-end' },
127
- 'Type to edit • Enter: save • Esc: cancel',
128
+ `Type ${
129
+ scalarType === 'string'
130
+ ? 'text'
131
+ : scalarType === 'integer'
132
+ ? 'integer'
133
+ : 'number'
134
+ } • Enter: save • Esc: cancel`,
128
135
  ),
129
136
  );
130
137
 
@@ -416,6 +423,10 @@ export const ConfigNavigator = ({
416
423
  );
417
424
  const shouldShowReadOnlyOptions =
418
425
  readOnlyOptions.length > 0 && !isBooleanOnlyOptions(readOnlyOptions);
426
+ const readOnlyValuePreviewText = getReadOnlyValuePreviewText(
427
+ selectedRow,
428
+ shouldShowReadOnlyOptions,
429
+ );
419
430
 
420
431
  const editRow = rows[selected] || null;
421
432
  const editDefaultOption =
@@ -471,8 +482,8 @@ export const ConfigNavigator = ({
471
482
  )
472
483
  : null;
473
484
  const optionSelector = editMode
474
- ? editMode.mode === 'text'
475
- ? renderTextEditor(editMode.draftValue)
485
+ ? editMode.mode === 'scalar'
486
+ ? renderScalarEditor(editMode.draftValue, editMode.scalarType)
476
487
  : editMode.mode === 'add-id'
477
488
  ? renderIdEditor(editMode.placeholder, editMode.draftValue)
478
489
  : renderEditableOptions(
@@ -497,6 +508,17 @@ export const ConfigNavigator = ({
497
508
  false,
498
509
  ),
499
510
  )
511
+ : readOnlyValuePreviewText
512
+ ? React.createElement(
513
+ React.Fragment,
514
+ null,
515
+ React.createElement(Text, { color: 'gray' }, ' '),
516
+ React.createElement(
517
+ Text,
518
+ { color: 'white', wrap: 'truncate-end' },
519
+ readOnlyValuePreviewText,
520
+ ),
521
+ )
500
522
  : selectedRow?.kind === 'array'
501
523
  ? renderArrayDetails(selectedRow.value)
502
524
  : null;
@@ -26,9 +26,4 @@ export const Header = ({ packageVersion }) =>
26
26
  `v${packageVersion || 'unknown'}`,
27
27
  ),
28
28
  ),
29
- React.createElement(
30
- Text,
31
- { color: 'white', dimColor: false },
32
- 'TUI Mode ',
33
- ),
34
29
  );
@@ -35,25 +35,24 @@ const buildFeatureDefinitionFromReference = (key) => {
35
35
  };
36
36
  };
37
37
 
38
- const DOCUMENTED_REFERENCE_FEATURE_KEYS = getReferenceFeatureKeys();
39
-
40
- export const CONFIG_FEATURE_DEFINITIONS = DOCUMENTED_REFERENCE_FEATURE_KEYS
41
- .map((key) => buildFeatureDefinitionFromReference(key))
42
- .filter(Boolean);
43
-
44
- const FEATURE_DEFINITION_MAP = CONFIG_FEATURE_DEFINITIONS.reduce((acc, definition) => {
45
- acc[definition.key] = definition;
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 DOCUMENTED_REFERENCE_FEATURE_KEYS;
48
+ return getReferenceFeatureKeys();
52
49
  };
53
50
 
54
- export const getConfigFeatureDefinition = (key) => FEATURE_DEFINITION_MAP[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
- FEATURE_DEFINITION_MAP[key] || {
68
- key,
69
- short: prettifyFeatureName(String(key)),
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,
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
 
3
3
  const require = createRequire(import.meta.url);
4
- const CONFIG_SCHEMA_DATA = require('./reference/config-schema.json');
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 referenceOptions = buildReferenceOptions(CONFIG_SCHEMA_DATA);
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 optionsByKey = new Map(referenceOptions.map((option) => [option.key, option]));
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 featureKeys = [
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