codex-configurator 0.1.0 → 0.2.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/README.md +52 -12
- package/index.js +121 -59
- package/package.json +53 -40
- package/src/components/ConfigNavigator.js +1 -1
- package/src/components/Header.js +2 -2
- package/src/configParser.js +122 -8
- package/src/configReference.js +4 -1
- package/src/customPathId.js +42 -0
- package/src/errorLogger.js +70 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Codex Configurator
|
|
2
2
|
|
|
3
3
|
Codex Configurator is a terminal user interface (TUI) built with Node.js, React, and Ink.
|
|
4
|
-
It shows the current contents of
|
|
4
|
+
It shows the current contents of a Codex TOML configuration file and can reload it on demand.
|
|
5
5
|
|
|
6
6
|
## Requirements
|
|
7
7
|
|
|
@@ -10,19 +10,26 @@ It shows the current contents of `~/.codex/config.toml` and can reload them on d
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm
|
|
13
|
+
npm i -g codex-configurator
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
codex-configurator
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Optional config-path override:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
codex-configurator
|
|
25
|
+
codex-configurator --config /path/to/config.toml
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For local development in this repository:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install
|
|
32
|
+
npm start
|
|
26
33
|
```
|
|
27
34
|
|
|
28
35
|
## Controls
|
|
@@ -33,7 +40,7 @@ codex-configurator
|
|
|
33
40
|
- `Enter`: open selected table; for boolean settings, toggle directly; for string settings, open inline input; for other preset values, open picker
|
|
34
41
|
- `Del`: unset selected value or remove selected custom `<id>` entry from `config.toml`
|
|
35
42
|
- `←` / `Backspace`: move up one level (to parent table)
|
|
36
|
-
- `r`: reload
|
|
43
|
+
- `r`: reload the active config file
|
|
37
44
|
- `q`: quit
|
|
38
45
|
|
|
39
46
|
The right-hand pane shows what each setting means, plus a picker when a value has preset options.
|
|
@@ -52,7 +59,7 @@ The table view follows TOML structure, with a root catalog of common keys:
|
|
|
52
59
|
- Selected sections such as `history`, `tui`, `feedback`, and `shell_environment_policy` also show common unset keys.
|
|
53
60
|
- Attributes and subattributes are shown in strict alphabetical order.
|
|
54
61
|
- Unset boolean settings display explicit defaults as `true [default]` or `false [default]`.
|
|
55
|
-
- For placeholder keys like `<path>`, IDs entered in the UI are normalized under your home directory.
|
|
62
|
+
- For placeholder keys like `<path>`, IDs entered in the UI are normalized under your home directory, and traversal outside home is rejected.
|
|
56
63
|
|
|
57
64
|
- Dotted/table sections become navigable table nodes.
|
|
58
65
|
- Inline key-value pairs are shown as leaf entries.
|
|
@@ -61,13 +68,39 @@ The table view follows TOML structure, with a root catalog of common keys:
|
|
|
61
68
|
|
|
62
69
|
## Configuration source
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
By default the app reads from:
|
|
65
72
|
|
|
66
73
|
```bash
|
|
67
74
|
~/.codex/config.toml
|
|
68
75
|
```
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
You can override this path with either:
|
|
78
|
+
|
|
79
|
+
- CLI: `--config /absolute/or/relative/path.toml`
|
|
80
|
+
- Env: `CODEX_CONFIGURATOR_CONFIG_PATH`
|
|
81
|
+
|
|
82
|
+
Precedence is CLI first, then environment variable, then the default path.
|
|
83
|
+
If the file is missing or unreadable, the TUI displays the read error and the resolved path.
|
|
84
|
+
Configuration writes are atomic and create the target file (and parent directories) when missing.
|
|
85
|
+
|
|
86
|
+
## Error logging
|
|
87
|
+
|
|
88
|
+
Read/write failures are appended to a JSON-lines log file.
|
|
89
|
+
|
|
90
|
+
- Default path: `~/.codex-configurator-errors.log`
|
|
91
|
+
- Override path: `CODEX_CONFIGURATOR_ERROR_LOG_PATH`
|
|
92
|
+
- Max log size before single-file rotation: `CODEX_CONFIGURATOR_ERROR_LOG_MAX_BYTES` (default `1048576`)
|
|
93
|
+
|
|
94
|
+
When the log exceeds the size limit, it is rotated to `<log-path>.1` before writing the new event.
|
|
95
|
+
|
|
96
|
+
## Version checks
|
|
97
|
+
|
|
98
|
+
Codex version checks are disabled by default to avoid executing unknown binaries from `PATH`.
|
|
99
|
+
To enable checks, all of the following must be set:
|
|
100
|
+
|
|
101
|
+
- `CODEX_CONFIGURATOR_ENABLE_VERSION_CHECK=1`
|
|
102
|
+
- `CODEX_CONFIGURATOR_CODEX_BIN=/absolute/path/to/codex`
|
|
103
|
+
- `CODEX_CONFIGURATOR_NPM_BIN=/absolute/path/to/npm`
|
|
71
104
|
|
|
72
105
|
## Upstream reference
|
|
73
106
|
|
|
@@ -77,9 +110,16 @@ If the file is missing or unreadable, the TUI displays the read error and the ex
|
|
|
77
110
|
|
|
78
111
|
- `npm start`: run the TUI
|
|
79
112
|
- `npm run dev`: same as `npm start`
|
|
80
|
-
- `npm run lint`:
|
|
81
|
-
- `npm run build`:
|
|
82
|
-
- `npm test`: runs
|
|
113
|
+
- `npm run lint`: ESLint static analysis for `index.js`, `src`, and `test`
|
|
114
|
+
- `npm run build`: validates the npm package archive (`npm pack --dry-run --ignore-scripts --cache .npm-cache`)
|
|
115
|
+
- `npm test`: runs the Node.js unit test suite (`node --test`)
|
|
116
|
+
|
|
117
|
+
## Continuous integration
|
|
118
|
+
|
|
119
|
+
GitHub Actions runs production dependency audit (`npm audit --omit=dev --audit-level=high`), `npm run lint`, `npm test`, `npm run build`, and `npm pack --dry-run` on every push and pull request across:
|
|
120
|
+
|
|
121
|
+
- `ubuntu-latest`, `macos-latest`, and `windows-latest`
|
|
122
|
+
- Node.js `18` and `20`
|
|
83
123
|
|
|
84
124
|
## Project structure
|
|
85
125
|
|
package/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
|
-
import
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { execSync } from 'node:child_process';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
7
6
|
import { render, useInput, useApp, useStdout, Text, Box } from 'ink';
|
|
8
7
|
import { CONTROL_HINT, EDIT_CONTROL_HINT } from './src/constants.js';
|
|
9
8
|
import {
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
getReferenceOptionForPath,
|
|
20
19
|
getReferenceCustomIdPlaceholder,
|
|
21
20
|
} from './src/configReference.js';
|
|
21
|
+
import { normalizeCustomPathId } from './src/customPathId.js';
|
|
22
22
|
import { pathToKey, clamp } from './src/layout.js';
|
|
23
23
|
import {
|
|
24
24
|
isBackspaceKey,
|
|
@@ -57,34 +57,69 @@ const isCustomIdTableRow = (pathSegments, row) =>
|
|
|
57
57
|
Boolean(getReferenceCustomIdPlaceholder(pathSegments));
|
|
58
58
|
|
|
59
59
|
const isInlineTextMode = (mode) => mode === 'text' || mode === 'add-id';
|
|
60
|
+
const VERSION_COMMAND_TIMEOUT_MS = 3000;
|
|
61
|
+
const VERSION_DISABLED_LABEL = 'version check disabled';
|
|
62
|
+
const VERSION_CHECK_ENABLED_ENV_VAR = 'CODEX_CONFIGURATOR_ENABLE_VERSION_CHECK';
|
|
63
|
+
const CODEX_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_CODEX_BIN';
|
|
64
|
+
const NPM_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_NPM_BIN';
|
|
65
|
+
|
|
66
|
+
const runCommand = (command, args = []) =>
|
|
67
|
+
new Promise((resolve) => {
|
|
68
|
+
execFile(
|
|
69
|
+
command,
|
|
70
|
+
args,
|
|
71
|
+
{
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
timeout: VERSION_COMMAND_TIMEOUT_MS,
|
|
74
|
+
maxBuffer: 1024 * 1024,
|
|
75
|
+
windowsHide: true,
|
|
76
|
+
},
|
|
77
|
+
(error, stdout) => {
|
|
78
|
+
if (error) {
|
|
79
|
+
resolve('');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
resolve(String(stdout || '').trim());
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
});
|
|
60
87
|
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
88
|
+
const getAbsoluteCommandPath = (environmentVariableName) => {
|
|
89
|
+
const configuredPath = String(process.env[environmentVariableName] || '').trim();
|
|
90
|
+
if (!configuredPath || !path.isAbsolute(configuredPath)) {
|
|
64
91
|
return '';
|
|
65
92
|
}
|
|
66
93
|
|
|
67
|
-
|
|
68
|
-
? trimmedValue.slice(1)
|
|
69
|
-
: trimmedValue;
|
|
70
|
-
const relativePath = withoutTilde.replace(/^[/\\]+/, '');
|
|
71
|
-
|
|
72
|
-
return path.join(os.homedir(), relativePath);
|
|
94
|
+
return configuredPath;
|
|
73
95
|
};
|
|
74
96
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
const getVersionCommands = () => {
|
|
98
|
+
if (process.env[VERSION_CHECK_ENABLED_ENV_VAR] !== '1') {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
const codexCommand = getAbsoluteCommandPath(CODEX_BIN_ENV_VAR);
|
|
103
|
+
const npmCommand = getAbsoluteCommandPath(NPM_BIN_ENV_VAR);
|
|
104
|
+
if (!codexCommand || !npmCommand) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
codexCommand,
|
|
110
|
+
npmCommand,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const getCodexVersion = async (codexCommand) => {
|
|
115
|
+
const output = await runCommand(codexCommand, ['--version']);
|
|
116
|
+
const firstLine = output.split('\n')[0]?.trim();
|
|
83
117
|
|
|
84
|
-
|
|
85
|
-
} catch {
|
|
118
|
+
if (!firstLine) {
|
|
86
119
|
return 'version unavailable';
|
|
87
120
|
}
|
|
121
|
+
|
|
122
|
+
return firstLine.startsWith('codex') ? firstLine : `version ${firstLine}`;
|
|
88
123
|
};
|
|
89
124
|
|
|
90
125
|
const normalizeVersion = (value) => {
|
|
@@ -120,53 +155,57 @@ const compareVersions = (left, right) => {
|
|
|
120
155
|
return 0;
|
|
121
156
|
};
|
|
122
157
|
|
|
123
|
-
const getCodexUpdateStatus = () => {
|
|
124
|
-
const
|
|
158
|
+
const getCodexUpdateStatus = async () => {
|
|
159
|
+
const commands = getVersionCommands();
|
|
160
|
+
if (!commands) {
|
|
161
|
+
return {
|
|
162
|
+
installed: VERSION_DISABLED_LABEL,
|
|
163
|
+
latest: 'unknown',
|
|
164
|
+
status: '',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const installedLabel = await getCodexVersion(commands.codexCommand);
|
|
169
|
+
const installed = normalizeVersion(installedLabel);
|
|
125
170
|
|
|
126
171
|
if (!installed) {
|
|
127
172
|
return {
|
|
128
|
-
installed:
|
|
173
|
+
installed: installedLabel,
|
|
129
174
|
latest: 'unknown',
|
|
130
175
|
status: 'version check unavailable',
|
|
131
176
|
};
|
|
132
177
|
}
|
|
133
178
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!latest) {
|
|
142
|
-
return {
|
|
143
|
-
installed,
|
|
144
|
-
latest: 'unknown',
|
|
145
|
-
status: 'version check unavailable',
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const comparison = compareVersions(installed, latest);
|
|
150
|
-
if (comparison < 0) {
|
|
151
|
-
return {
|
|
152
|
-
installed,
|
|
153
|
-
latest,
|
|
154
|
-
status: 'update available',
|
|
155
|
-
};
|
|
156
|
-
}
|
|
179
|
+
const latestOutput = await runCommand(commands.npmCommand, [
|
|
180
|
+
'view',
|
|
181
|
+
'@openai/codex',
|
|
182
|
+
'version',
|
|
183
|
+
'--json',
|
|
184
|
+
]);
|
|
185
|
+
const latest = normalizeVersion(latestOutput) || latestOutput.trim();
|
|
157
186
|
|
|
187
|
+
if (!latest) {
|
|
158
188
|
return {
|
|
159
189
|
installed,
|
|
160
|
-
latest,
|
|
161
|
-
status: '
|
|
190
|
+
latest: 'unknown',
|
|
191
|
+
status: 'version check unavailable',
|
|
162
192
|
};
|
|
163
|
-
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const comparison = compareVersions(installed, latest);
|
|
196
|
+
if (comparison < 0) {
|
|
164
197
|
return {
|
|
165
198
|
installed,
|
|
166
|
-
latest
|
|
167
|
-
status: '
|
|
199
|
+
latest,
|
|
200
|
+
status: 'update available',
|
|
168
201
|
};
|
|
169
202
|
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
installed,
|
|
206
|
+
latest,
|
|
207
|
+
status: 'up to date',
|
|
208
|
+
};
|
|
170
209
|
};
|
|
171
210
|
|
|
172
211
|
const App = () => {
|
|
@@ -187,9 +226,24 @@ const App = () => {
|
|
|
187
226
|
const { exit } = useApp();
|
|
188
227
|
|
|
189
228
|
useEffect(() => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
229
|
+
let isCancelled = false;
|
|
230
|
+
|
|
231
|
+
const loadVersionStatus = async () => {
|
|
232
|
+
const check = await getCodexUpdateStatus();
|
|
233
|
+
|
|
234
|
+
if (isCancelled) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
setCodexVersion(check.installed);
|
|
239
|
+
setCodexVersionStatus(check.status);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
loadVersionStatus();
|
|
243
|
+
|
|
244
|
+
return () => {
|
|
245
|
+
isCancelled = true;
|
|
246
|
+
};
|
|
193
247
|
}, []);
|
|
194
248
|
|
|
195
249
|
const currentNode = getNodeAtPath(snapshot.ok ? snapshot.data : {}, pathSegments);
|
|
@@ -316,9 +370,17 @@ const App = () => {
|
|
|
316
370
|
|
|
317
371
|
const nextIdInput = String(editMode.draftValue || '').trim();
|
|
318
372
|
const placeholder = getReferenceCustomIdPlaceholder(editMode.path);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
373
|
+
let nextId = nextIdInput;
|
|
374
|
+
|
|
375
|
+
if (placeholder === '<path>') {
|
|
376
|
+
const normalizedPath = normalizeCustomPathId(nextIdInput);
|
|
377
|
+
if (!normalizedPath.ok) {
|
|
378
|
+
setEditError(normalizedPath.error);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
nextId = normalizedPath.value;
|
|
383
|
+
}
|
|
322
384
|
|
|
323
385
|
if (!nextId) {
|
|
324
386
|
setEditError('ID cannot be empty.');
|
|
@@ -712,7 +774,7 @@ const App = () => {
|
|
|
712
774
|
setEditError('');
|
|
713
775
|
return;
|
|
714
776
|
}
|
|
715
|
-
});
|
|
777
|
+
}, { isActive: isInteractive });
|
|
716
778
|
|
|
717
779
|
useEffect(() => {
|
|
718
780
|
const maxOffset = Math.max(0, rows.length - listViewportHeight);
|
package/package.json
CHANGED
|
@@ -1,42 +1,55 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
2
|
+
"name": "codex-configurator",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "TOML-aware Ink TUI for Codex Configurator",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"tui",
|
|
9
|
+
"terminal",
|
|
10
|
+
"cli",
|
|
11
|
+
"react",
|
|
12
|
+
"ink",
|
|
13
|
+
"node",
|
|
14
|
+
"configurator"
|
|
15
|
+
],
|
|
16
|
+
"author": "Codex Configurator",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/enriquemorenotent/codex-configurator.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/enriquemorenotent/codex-configurator#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/enriquemorenotent/codex-configurator/issues"
|
|
25
|
+
},
|
|
26
|
+
"funding": "https://github.com/sponsors/enriquemorenotent",
|
|
27
|
+
"bin": {
|
|
28
|
+
"codex-configurator": "index.js"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"start": "node index.js",
|
|
32
|
+
"dev": "node index.js",
|
|
33
|
+
"lint": "eslint index.js src test --max-warnings=0",
|
|
34
|
+
"build": "npm pack --dry-run --ignore-scripts --cache .npm-cache",
|
|
35
|
+
"test": "node --test",
|
|
36
|
+
"prepack": "npm run lint && npm test"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"packageManager": "npm@10.9.4",
|
|
42
|
+
"files": [
|
|
43
|
+
"index.js",
|
|
44
|
+
"src"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@iarna/toml": "^2.2.5",
|
|
48
|
+
"ink": "^6.2.1",
|
|
49
|
+
"react": "^19.0.0",
|
|
50
|
+
"toml": "^3.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"eslint": "^8.57.1"
|
|
54
|
+
}
|
|
42
55
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
getConfigDefaultOption,
|
|
8
8
|
} from '../configHelp.js';
|
|
9
9
|
import { computePaneWidths, clamp } from '../layout.js';
|
|
10
|
-
import { getNodeAtPath, buildRows
|
|
10
|
+
import { getNodeAtPath, buildRows } from '../configParser.js';
|
|
11
11
|
|
|
12
12
|
const MenuItem = ({ isSelected, isDimmed, isDeprecated, label }) =>
|
|
13
13
|
React.createElement(
|
package/src/components/Header.js
CHANGED
|
@@ -21,8 +21,8 @@ export const Header = ({ codexVersion, codexVersionStatus }) =>
|
|
|
21
21
|
React.createElement(
|
|
22
22
|
Box,
|
|
23
23
|
{ flexDirection: 'column', marginBottom: 1, gap: 0 },
|
|
24
|
-
...WORDMARK.map((line) =>
|
|
25
|
-
React.createElement(Text, { color: 'magentaBright', bold: true, key: `word-${
|
|
24
|
+
...WORDMARK.map((line, index) =>
|
|
25
|
+
React.createElement(Text, { color: 'magentaBright', bold: true, key: `word-${index}` }, line)
|
|
26
26
|
)
|
|
27
27
|
),
|
|
28
28
|
React.createElement(
|
package/src/configParser.js
CHANGED
|
@@ -13,8 +13,12 @@ import {
|
|
|
13
13
|
getReferenceRootDefinitions,
|
|
14
14
|
getReferenceTableDefinitions,
|
|
15
15
|
} from './configReference.js';
|
|
16
|
+
import { logConfiguratorError } from './errorLogger.js';
|
|
16
17
|
|
|
17
|
-
export const
|
|
18
|
+
export const CONFIG_PATH_ENV_VAR = 'CODEX_CONFIGURATOR_CONFIG_PATH';
|
|
19
|
+
const CONFIG_PATH_FLAG = '--config';
|
|
20
|
+
const DEFAULT_CONFIG_FILE_SEGMENTS = ['.codex', 'config.toml'];
|
|
21
|
+
export const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ...DEFAULT_CONFIG_FILE_SEGMENTS);
|
|
18
22
|
export const MAX_DETAIL_CHARS = 2200;
|
|
19
23
|
const MAX_ARRAY_PREVIEW_ITEMS = 3;
|
|
20
24
|
const MAX_ARRAY_PREVIEW_CHARS = 52;
|
|
@@ -77,21 +81,91 @@ const formatArrayPreview = (value) => {
|
|
|
77
81
|
return truncateText(joined, MAX_ARRAY_PREVIEW_CHARS);
|
|
78
82
|
};
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
const expandHomePath = (value, homeDir) => {
|
|
85
|
+
if (value === '~') {
|
|
86
|
+
return homeDir;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (value.startsWith('~/') || value.startsWith('~\\')) {
|
|
90
|
+
return path.join(homeDir, value.slice(2));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return value;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const normalizeConfiguredPath = (value, homeDir) => {
|
|
97
|
+
const normalizedValue = typeof value === 'string' ? value.trim() : '';
|
|
98
|
+
if (!normalizedValue) {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const expanded = expandHomePath(normalizedValue, homeDir);
|
|
103
|
+
return path.resolve(expanded);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const getCliConfigPath = (argv = process.argv.slice(2)) => {
|
|
107
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
108
|
+
const argument = typeof argv[index] === 'string' ? argv[index].trim() : '';
|
|
109
|
+
if (!argument) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (argument === CONFIG_PATH_FLAG) {
|
|
114
|
+
const nextArgument = argv[index + 1];
|
|
115
|
+
return typeof nextArgument === 'string' ? nextArgument : '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (argument.startsWith(`${CONFIG_PATH_FLAG}=`)) {
|
|
119
|
+
return argument.slice(CONFIG_PATH_FLAG.length + 1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return '';
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const resolveConfigPath = ({
|
|
127
|
+
argv = process.argv.slice(2),
|
|
128
|
+
env = process.env,
|
|
129
|
+
homeDir = os.homedir(),
|
|
130
|
+
} = {}) => {
|
|
131
|
+
const cliConfiguredPath = normalizeConfiguredPath(getCliConfigPath(argv), homeDir);
|
|
132
|
+
if (cliConfiguredPath) {
|
|
133
|
+
return cliConfiguredPath;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const envConfiguredPath = normalizeConfiguredPath(env?.[CONFIG_PATH_ENV_VAR], homeDir);
|
|
137
|
+
if (envConfiguredPath) {
|
|
138
|
+
return envConfiguredPath;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return path.join(homeDir, ...DEFAULT_CONFIG_FILE_SEGMENTS);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const CONFIG_PATH = resolveConfigPath();
|
|
145
|
+
|
|
146
|
+
export const readConfig = (configPath = CONFIG_PATH) => {
|
|
81
147
|
try {
|
|
82
|
-
const
|
|
148
|
+
const targetPath = configPath;
|
|
149
|
+
const fileContents = fs.readFileSync(targetPath, 'utf8');
|
|
83
150
|
const data = toml.parse(fileContents);
|
|
84
151
|
|
|
85
152
|
return {
|
|
86
153
|
ok: true,
|
|
87
|
-
path:
|
|
154
|
+
path: targetPath,
|
|
88
155
|
data,
|
|
89
156
|
};
|
|
90
157
|
} catch (error) {
|
|
158
|
+
const targetPath = configPath;
|
|
159
|
+
const errorMessage = error?.message || 'Unable to read or parse configuration file.';
|
|
160
|
+
logConfiguratorError('config.read.failed', {
|
|
161
|
+
configPath: targetPath,
|
|
162
|
+
error: errorMessage,
|
|
163
|
+
});
|
|
164
|
+
|
|
91
165
|
return {
|
|
92
166
|
ok: false,
|
|
93
|
-
path:
|
|
94
|
-
error:
|
|
167
|
+
path: targetPath,
|
|
168
|
+
error: errorMessage,
|
|
95
169
|
};
|
|
96
170
|
}
|
|
97
171
|
};
|
|
@@ -99,17 +173,57 @@ export const readConfig = () => {
|
|
|
99
173
|
const normalizeFilePath = (outputPath) => outputPath || CONFIG_PATH;
|
|
100
174
|
|
|
101
175
|
export const writeConfig = (data, outputPath = CONFIG_PATH) => {
|
|
176
|
+
const targetPath = normalizeFilePath(outputPath);
|
|
177
|
+
const directoryPath = path.dirname(targetPath);
|
|
178
|
+
const fileName = path.basename(targetPath);
|
|
179
|
+
const tempPath = path.join(
|
|
180
|
+
directoryPath,
|
|
181
|
+
`.${fileName}.${process.pid}.${Date.now()}.tmp`
|
|
182
|
+
);
|
|
183
|
+
|
|
102
184
|
try {
|
|
185
|
+
if (!fs.existsSync(directoryPath)) {
|
|
186
|
+
fs.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
|
|
187
|
+
}
|
|
188
|
+
|
|
103
189
|
const payload = stringify(data);
|
|
104
|
-
|
|
190
|
+
let tempFd = null;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
tempFd = fs.openSync(tempPath, 'wx', 0o600);
|
|
194
|
+
fs.writeFileSync(tempFd, `${payload}\n`, 'utf8');
|
|
195
|
+
fs.fsyncSync(tempFd);
|
|
196
|
+
fs.closeSync(tempFd);
|
|
197
|
+
tempFd = null;
|
|
198
|
+
|
|
199
|
+
fs.renameSync(tempPath, targetPath);
|
|
200
|
+
} finally {
|
|
201
|
+
if (tempFd !== null) {
|
|
202
|
+
try {
|
|
203
|
+
fs.closeSync(tempFd);
|
|
204
|
+
} catch {}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (fs.existsSync(tempPath)) {
|
|
208
|
+
try {
|
|
209
|
+
fs.unlinkSync(tempPath);
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
105
213
|
|
|
106
214
|
return {
|
|
107
215
|
ok: true,
|
|
108
216
|
};
|
|
109
217
|
} catch (error) {
|
|
218
|
+
const errorMessage = error?.message || 'Unable to write configuration file.';
|
|
219
|
+
logConfiguratorError('config.write.failed', {
|
|
220
|
+
configPath: targetPath,
|
|
221
|
+
error: errorMessage,
|
|
222
|
+
});
|
|
223
|
+
|
|
110
224
|
return {
|
|
111
225
|
ok: false,
|
|
112
|
-
error:
|
|
226
|
+
error: errorMessage,
|
|
113
227
|
};
|
|
114
228
|
}
|
|
115
229
|
};
|
package/src/configReference.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const CONFIG_REFERENCE_DATA = require('./reference/config-reference.json');
|
|
2
5
|
|
|
3
6
|
const DOCUMENT_ID = 'config.toml';
|
|
4
7
|
const PLACEHOLDER_SEGMENT = /^<[^>]+>$/;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const normalizePathSegments = (value) =>
|
|
5
|
+
String(value || '')
|
|
6
|
+
.split(/[\\/]+/)
|
|
7
|
+
.filter((segment) => segment.length > 0)
|
|
8
|
+
.join(path.sep);
|
|
9
|
+
|
|
10
|
+
const resolveCustomPath = (value, homePath) => {
|
|
11
|
+
const withoutTilde = value.startsWith('~') ? value.slice(1) : value;
|
|
12
|
+
const relativePath = normalizePathSegments(withoutTilde.replace(/^[/\\]+/, ''));
|
|
13
|
+
return path.resolve(homePath, relativePath);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isWithinHomePath = (resolvedPath, homePath) => {
|
|
17
|
+
const relative = path.relative(homePath, resolvedPath);
|
|
18
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const normalizeCustomPathId = (value, homePath = os.homedir()) => {
|
|
22
|
+
const trimmedValue = String(value || '').trim();
|
|
23
|
+
if (!trimmedValue) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: 'ID cannot be empty.',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const resolvedPath = resolveCustomPath(trimmedValue, homePath);
|
|
31
|
+
if (!isWithinHomePath(resolvedPath, homePath)) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error: `Path must stay inside ${homePath}.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
value: resolvedPath,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_ERROR_LOG_PATH = path.join(os.homedir(), '.codex-configurator-errors.log');
|
|
6
|
+
const DEFAULT_ERROR_LOG_MAX_BYTES = 1024 * 1024;
|
|
7
|
+
const ERROR_LOG_MAX_BYTES_ENV_VAR = 'CODEX_CONFIGURATOR_ERROR_LOG_MAX_BYTES';
|
|
8
|
+
|
|
9
|
+
const getErrorLogPath = () =>
|
|
10
|
+
process.env.CODEX_CONFIGURATOR_ERROR_LOG_PATH || DEFAULT_ERROR_LOG_PATH;
|
|
11
|
+
|
|
12
|
+
const parsePositiveInteger = (value) => {
|
|
13
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
14
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parsed;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getErrorLogMaxBytes = () =>
|
|
22
|
+
parsePositiveInteger(process.env[ERROR_LOG_MAX_BYTES_ENV_VAR]) || DEFAULT_ERROR_LOG_MAX_BYTES;
|
|
23
|
+
|
|
24
|
+
const ensureLogDirectory = (logPath) => {
|
|
25
|
+
const directoryPath = path.dirname(logPath);
|
|
26
|
+
if (!fs.existsSync(directoryPath)) {
|
|
27
|
+
fs.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const rotateLogFileIfNeeded = (logPath, maxBytes) => {
|
|
32
|
+
try {
|
|
33
|
+
const stats = fs.statSync(logPath);
|
|
34
|
+
if (!stats.isFile() || stats.size < maxBytes) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rotatedPath = `${logPath}.1`;
|
|
39
|
+
if (fs.existsSync(rotatedPath)) {
|
|
40
|
+
fs.unlinkSync(rotatedPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fs.renameSync(logPath, rotatedPath);
|
|
44
|
+
} catch {}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const logConfiguratorError = (event, details = {}) => {
|
|
48
|
+
const payload = {
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
event: String(event || 'unknown'),
|
|
51
|
+
...details,
|
|
52
|
+
};
|
|
53
|
+
const logPath = getErrorLogPath();
|
|
54
|
+
const maxBytes = getErrorLogMaxBytes();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
ensureLogDirectory(logPath);
|
|
58
|
+
rotateLogFileIfNeeded(logPath, maxBytes);
|
|
59
|
+
fs.appendFileSync(logPath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
path: logPath,
|
|
63
|
+
};
|
|
64
|
+
} catch {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
path: logPath,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
};
|