@orionpotter/menv 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -0
- package/dist/cli.js +223 -0
- package/dist/clients/codex.js +154 -0
- package/dist/clients/index.js +19 -0
- package/dist/config/store.js +119 -0
- package/dist/core/resolver.js +48 -0
- package/dist/errors.js +19 -0
- package/dist/index.js +5 -0
- package/dist/output.js +74 -0
- package/dist/types.js +2 -0
- package/dist/utils/fs.js +55 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# menv
|
|
2
|
+
|
|
3
|
+
`mm` is a profile manager for AI CLI tools.
|
|
4
|
+
|
|
5
|
+
It manages provider profiles such as `openai`, `openrouter`, or custom gateways, then applies the selected profile to a downstream CLI client. The current MVP supports `codex` by updating `~/.codex/config.toml`.
|
|
6
|
+
|
|
7
|
+
## What problem it solves
|
|
8
|
+
|
|
9
|
+
If you regularly switch between different model endpoints, API keys, or models, you usually end up editing one of these by hand:
|
|
10
|
+
|
|
11
|
+
- shell environment variables
|
|
12
|
+
- `.env` files
|
|
13
|
+
- client config files such as `~/.codex/config.toml`
|
|
14
|
+
- ad hoc scripts for each provider
|
|
15
|
+
|
|
16
|
+
`mm` gives you a single place to define named profiles and switch clients between them safely.
|
|
17
|
+
|
|
18
|
+
## Core concepts
|
|
19
|
+
|
|
20
|
+
- `profile`: one saved configuration bundle, for example `openai-main` or `router-fast`
|
|
21
|
+
- `client`: one downstream CLI whose config file can be managed by `mm`
|
|
22
|
+
- `use`: apply one profile to one client
|
|
23
|
+
- `doctor`: compare the selected profile with the actual client config and report drift
|
|
24
|
+
- `sync`: re-apply the selected profile to the client config
|
|
25
|
+
- `rollback`: restore the most recent backup created during `use` or `sync`
|
|
26
|
+
|
|
27
|
+
## Current MVP scope
|
|
28
|
+
|
|
29
|
+
Implemented now:
|
|
30
|
+
|
|
31
|
+
- profile storage in `mm` config
|
|
32
|
+
- alias support
|
|
33
|
+
- client abstraction for future expansion
|
|
34
|
+
- Codex client support
|
|
35
|
+
- config drift detection
|
|
36
|
+
- one-step sync back to Codex config
|
|
37
|
+
- backup and rollback
|
|
38
|
+
- npm package with `mm` executable
|
|
39
|
+
|
|
40
|
+
Not implemented yet:
|
|
41
|
+
|
|
42
|
+
- multiple backup history
|
|
43
|
+
- encrypted secret storage
|
|
44
|
+
- direct model invocation through `mm`
|
|
45
|
+
- non-Codex client adapters
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
Local development:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install
|
|
53
|
+
npm link
|
|
54
|
+
mm.cmd --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
After publishing to npm:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install -g @orionpotter/menv
|
|
61
|
+
mm --help
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Notes:
|
|
65
|
+
|
|
66
|
+
- On Windows PowerShell with restrictive execution policy, use `mm.cmd`.
|
|
67
|
+
- The npm package name is `@orionpotter/menv` while the executable command remains `mm`.
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
### 1. Add profiles
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
mm add openai-main --provider openai --model gpt-5.4 --base-url https://api.openai.com/v1 --api-key-env OPENAI_API_KEY
|
|
75
|
+
mm add router-fast --provider openrouter --model openai/gpt-4o-mini --base-url https://openrouter.ai/api/v1 --api-key-env OPENROUTER_API_KEY
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2. Point Codex at one profile
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
mm use openai-main --client codex
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This updates `~/.codex/config.toml` so Codex will use the selected profile values.
|
|
85
|
+
|
|
86
|
+
### 3. Inspect current state
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
mm current --client codex
|
|
90
|
+
mm which --client codex
|
|
91
|
+
mm doctor --client codex
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 4. Re-sync after drift
|
|
95
|
+
|
|
96
|
+
If you manually edited `~/.codex/config.toml` or another tool changed it:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
mm sync --client codex
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 5. Roll back to the last backup
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
mm rollback --client codex
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Concrete Codex example
|
|
109
|
+
|
|
110
|
+
Suppose your current `~/.codex/config.toml` points to one endpoint, but you want to temporarily switch Codex to OpenRouter.
|
|
111
|
+
|
|
112
|
+
Create a profile:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
mm add router-fast \
|
|
116
|
+
--provider openrouter \
|
|
117
|
+
--model openai/gpt-4o-mini \
|
|
118
|
+
--base-url https://openrouter.ai/api/v1 \
|
|
119
|
+
--api-key-env OPENROUTER_API_KEY
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Apply it to Codex:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
mm use router-fast --client codex
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Now inspect the result:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
mm which --client codex
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
You should see the selected profile values and the actual values found in `~/.codex/config.toml`.
|
|
135
|
+
|
|
136
|
+
If they drift apart later:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
mm doctor --client codex
|
|
140
|
+
mm sync --client codex
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
If you want to go back to the previous Codex config snapshot:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
mm rollback --client codex
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Alias example
|
|
150
|
+
|
|
151
|
+
Aliases let you switch by intent instead of provider details.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
mm alias set fast router-fast
|
|
155
|
+
mm use fast --client codex
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Commands
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
mm list
|
|
162
|
+
mm clients
|
|
163
|
+
mm current [--client codex]
|
|
164
|
+
mm which [--client codex]
|
|
165
|
+
mm use <profile> [--client codex] [--project]
|
|
166
|
+
mm sync [--client codex]
|
|
167
|
+
mm doctor [--client codex]
|
|
168
|
+
mm rollback [--client codex]
|
|
169
|
+
mm add <profile> --provider NAME --model MODEL --base-url URL [--api-key KEY] [--api-key-env ENV]
|
|
170
|
+
mm remove <profile>
|
|
171
|
+
mm config set <key> <value>
|
|
172
|
+
mm config get <key>
|
|
173
|
+
mm config list
|
|
174
|
+
mm alias list
|
|
175
|
+
mm alias set <name> <profile>
|
|
176
|
+
mm alias remove <name>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## How `mm use` works
|
|
180
|
+
|
|
181
|
+
`mm use <profile> --client codex` does two things:
|
|
182
|
+
|
|
183
|
+
1. It records the selected profile in `mm`'s own config.
|
|
184
|
+
2. It applies that profile to Codex by updating `~/.codex/config.toml`.
|
|
185
|
+
|
|
186
|
+
For Codex, the current implementation writes these top-level keys when present:
|
|
187
|
+
|
|
188
|
+
- `provider`
|
|
189
|
+
- `model`
|
|
190
|
+
- `base_url`
|
|
191
|
+
- `api_key`
|
|
192
|
+
- `api_key_env`
|
|
193
|
+
- `model_reasoning_effort`
|
|
194
|
+
|
|
195
|
+
Existing unrelated TOML sections are preserved.
|
|
196
|
+
|
|
197
|
+
## Backup and rollback semantics
|
|
198
|
+
|
|
199
|
+
Before `mm use` or `mm sync` writes the client config, `mm` creates one backup file:
|
|
200
|
+
|
|
201
|
+
- Codex backup path: `~/.codex/config.toml.bak`
|
|
202
|
+
|
|
203
|
+
`mm rollback --client codex` restores that backup.
|
|
204
|
+
|
|
205
|
+
Important limitation in the current MVP:
|
|
206
|
+
|
|
207
|
+
- backup history is single-slot
|
|
208
|
+
- the latest write replaces the previous backup
|
|
209
|
+
- if you sync after a bad manual edit, rollback restores the state immediately before that sync, not an older historical version
|
|
210
|
+
|
|
211
|
+
## Validation behavior
|
|
212
|
+
|
|
213
|
+
For Codex, `mm doctor` currently checks:
|
|
214
|
+
|
|
215
|
+
- whether the selected profile has a `model`
|
|
216
|
+
- whether `baseURL` is missing
|
|
217
|
+
- whether both `apiKey` and `apiKeyEnv` are missing
|
|
218
|
+
- whether the current Codex config has drifted from the selected profile
|
|
219
|
+
|
|
220
|
+
## Config files
|
|
221
|
+
|
|
222
|
+
`mm` config:
|
|
223
|
+
|
|
224
|
+
- Windows: `%APPDATA%/mm/config.json`
|
|
225
|
+
- macOS/Linux: `~/.config/mm/config.json`
|
|
226
|
+
|
|
227
|
+
Current supported client config:
|
|
228
|
+
|
|
229
|
+
- Codex: `~/.codex/config.toml`
|
|
230
|
+
|
|
231
|
+
Project override file:
|
|
232
|
+
|
|
233
|
+
- `.mm.json`
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npm install
|
|
239
|
+
npm run check
|
|
240
|
+
npm run build
|
|
241
|
+
npm link
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Publish to npm
|
|
245
|
+
|
|
246
|
+
Manual publish:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npm login
|
|
250
|
+
npm publish --provenance
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
GitHub Actions publish:
|
|
254
|
+
|
|
255
|
+
- workflow file: `.github/workflows/publish.yml`
|
|
256
|
+
- trigger: push tag matching `v*` or manual dispatch
|
|
257
|
+
- required secret: `NPM_TOKEN`
|
|
258
|
+
|
|
259
|
+
## Roadmap
|
|
260
|
+
|
|
261
|
+
Planned next steps:
|
|
262
|
+
|
|
263
|
+
- multiple backup history and named rollback targets
|
|
264
|
+
- more client adapters beyond Codex
|
|
265
|
+
- safer secret handling
|
|
266
|
+
- richer profile schema per client
|
|
267
|
+
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCli = runCli;
|
|
4
|
+
exports.handleCliError = handleCliError;
|
|
5
|
+
const store_1 = require("./config/store");
|
|
6
|
+
const resolver_1 = require("./core/resolver");
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
const clients_1 = require("./clients");
|
|
9
|
+
const output_1 = require("./output");
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const positionals = [];
|
|
12
|
+
const flags = {};
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
const token = argv[index];
|
|
15
|
+
if (!token.startsWith("--")) {
|
|
16
|
+
positionals.push(token);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const key = token.slice(2);
|
|
20
|
+
const next = argv[index + 1];
|
|
21
|
+
if (!next || next.startsWith("--")) {
|
|
22
|
+
flags[key] = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
flags[key] = next;
|
|
26
|
+
index += 1;
|
|
27
|
+
}
|
|
28
|
+
return { positionals, flags };
|
|
29
|
+
}
|
|
30
|
+
function asString(flag) {
|
|
31
|
+
return typeof flag === "string" ? flag : undefined;
|
|
32
|
+
}
|
|
33
|
+
function requirePositional(value, message) {
|
|
34
|
+
if (!value) {
|
|
35
|
+
throw new errors_1.ConfigError(message);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function printHelp() {
|
|
40
|
+
console.log(`mm - client config manager
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
mm list
|
|
44
|
+
mm clients
|
|
45
|
+
mm current [--client codex]
|
|
46
|
+
mm which [--client codex]
|
|
47
|
+
mm use <profile> [--client codex] [--project]
|
|
48
|
+
mm sync [--client codex]
|
|
49
|
+
mm doctor [--client codex]
|
|
50
|
+
mm rollback [--client codex]
|
|
51
|
+
mm add <profile> --provider NAME --model MODEL --base-url URL [--api-key KEY] [--api-key-env ENV]
|
|
52
|
+
mm remove <profile>
|
|
53
|
+
mm config set <key> <value>
|
|
54
|
+
mm config get <key>
|
|
55
|
+
mm config list
|
|
56
|
+
mm alias list
|
|
57
|
+
mm alias set <name> <profile>
|
|
58
|
+
mm alias remove <name>`);
|
|
59
|
+
}
|
|
60
|
+
function getClientName(flagValue, storeDefault) {
|
|
61
|
+
return flagValue ?? storeDefault;
|
|
62
|
+
}
|
|
63
|
+
async function runCli(argv) {
|
|
64
|
+
const store = new store_1.ConfigStore();
|
|
65
|
+
const resolver = new resolver_1.Resolver(store);
|
|
66
|
+
const [command, ...rest] = argv;
|
|
67
|
+
const parsed = parseArgs(rest);
|
|
68
|
+
const config = await store.readGlobalConfig();
|
|
69
|
+
const clientName = getClientName(asString(parsed.flags.client), config.defaultClient);
|
|
70
|
+
switch (command) {
|
|
71
|
+
case undefined:
|
|
72
|
+
case "help":
|
|
73
|
+
case "--help":
|
|
74
|
+
printHelp();
|
|
75
|
+
return;
|
|
76
|
+
case "clients": {
|
|
77
|
+
for (const client of (0, clients_1.listClientNames)()) {
|
|
78
|
+
console.log(client);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
case "list": {
|
|
83
|
+
(0, output_1.printList)(config);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
case "current": {
|
|
87
|
+
const resolved = await resolver.resolveProfile({ client: clientName });
|
|
88
|
+
const state = await (0, clients_1.getClientAdapter)(clientName).readState();
|
|
89
|
+
(0, output_1.printCurrent)(resolved, state);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
case "which": {
|
|
93
|
+
const resolved = await resolver.resolveProfile({ client: clientName });
|
|
94
|
+
const state = await (0, clients_1.getClientAdapter)(clientName).readState();
|
|
95
|
+
(0, output_1.printWhich)(resolved, state);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
case "use": {
|
|
99
|
+
const profileName = requirePositional(parsed.positionals[0], "Usage: mm use <profile>");
|
|
100
|
+
const scope = parsed.flags.project ? "project" : "global";
|
|
101
|
+
await store.setCurrentProfile(clientName, profileName, scope);
|
|
102
|
+
const client = (0, clients_1.getClientAdapter)(clientName);
|
|
103
|
+
const resolved = await resolver.resolveProfile({ client: clientName });
|
|
104
|
+
const validationIssues = client.validateProfile(resolved.profile).filter((issue) => issue.level === "error");
|
|
105
|
+
if (validationIssues.length > 0) {
|
|
106
|
+
(0, output_1.printIssues)(validationIssues);
|
|
107
|
+
throw new errors_1.ConfigError("Profile validation failed.");
|
|
108
|
+
}
|
|
109
|
+
const result = await client.applyProfile(resolved.profileName, resolved.profile);
|
|
110
|
+
console.log(`updated ${scope} profile for ${clientName} to ${profileName}`);
|
|
111
|
+
(0, output_1.printApplyResult)(result);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
case "sync": {
|
|
115
|
+
const client = (0, clients_1.getClientAdapter)(clientName);
|
|
116
|
+
const resolved = await resolver.resolveProfile({ client: clientName });
|
|
117
|
+
const validationIssues = client.validateProfile(resolved.profile).filter((issue) => issue.level === "error");
|
|
118
|
+
if (validationIssues.length > 0) {
|
|
119
|
+
(0, output_1.printIssues)(validationIssues);
|
|
120
|
+
throw new errors_1.ConfigError("Profile validation failed.");
|
|
121
|
+
}
|
|
122
|
+
const result = await client.applyProfile(resolved.profileName, resolved.profile);
|
|
123
|
+
(0, output_1.printApplyResult)(result);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
case "doctor": {
|
|
127
|
+
const client = (0, clients_1.getClientAdapter)(clientName);
|
|
128
|
+
const resolved = await resolver.resolveProfile({ client: clientName });
|
|
129
|
+
const state = await client.readState();
|
|
130
|
+
const issues = [
|
|
131
|
+
...client.validateProfile(resolved.profile),
|
|
132
|
+
...client.compareProfile(state, resolved.profile)
|
|
133
|
+
];
|
|
134
|
+
(0, output_1.printIssues)(issues);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
case "rollback": {
|
|
138
|
+
const result = await (0, clients_1.getClientAdapter)(clientName).rollback();
|
|
139
|
+
(0, output_1.printRollbackResult)(result);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
case "add": {
|
|
143
|
+
const profileName = requirePositional(parsed.positionals[0], "Usage: mm add <profile> --provider NAME --model MODEL --base-url URL");
|
|
144
|
+
await store.upsertProfile(profileName, {
|
|
145
|
+
provider: asString(parsed.flags.provider),
|
|
146
|
+
model: asString(parsed.flags.model),
|
|
147
|
+
baseURL: asString(parsed.flags["base-url"]),
|
|
148
|
+
apiKey: asString(parsed.flags["api-key"]),
|
|
149
|
+
apiKeyEnv: asString(parsed.flags["api-key-env"]),
|
|
150
|
+
reasoningEffort: asString(parsed.flags["reasoning-effort"]),
|
|
151
|
+
organization: asString(parsed.flags.organization),
|
|
152
|
+
region: asString(parsed.flags.region),
|
|
153
|
+
deployment: asString(parsed.flags.deployment)
|
|
154
|
+
});
|
|
155
|
+
console.log(`profile ${profileName} updated`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
case "remove": {
|
|
159
|
+
const profileName = requirePositional(parsed.positionals[0], "Usage: mm remove <profile>");
|
|
160
|
+
await store.removeProfile(profileName);
|
|
161
|
+
console.log(`profile ${profileName} removed`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
case "config": {
|
|
165
|
+
const subcommand = parsed.positionals[0];
|
|
166
|
+
if (subcommand === "set") {
|
|
167
|
+
const key = requirePositional(parsed.positionals[1], "Usage: mm config set <key> <value>");
|
|
168
|
+
const value = requirePositional(parsed.positionals[2], "Usage: mm config set <key> <value>");
|
|
169
|
+
await store.setConfigValue(key, value);
|
|
170
|
+
console.log(`config updated: ${key}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (subcommand === "get") {
|
|
174
|
+
const key = requirePositional(parsed.positionals[1], "Usage: mm config get <key>");
|
|
175
|
+
const value = await store.getConfigValue(key);
|
|
176
|
+
console.log(value === undefined ? "(unset)" : JSON.stringify(value, null, 2));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (subcommand === "list") {
|
|
180
|
+
(0, output_1.printConfig)(config);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
throw new errors_1.ConfigError("Usage: mm config <set|get|list> ...");
|
|
184
|
+
}
|
|
185
|
+
case "alias": {
|
|
186
|
+
const subcommand = parsed.positionals[0];
|
|
187
|
+
if (subcommand === "list") {
|
|
188
|
+
for (const [name, target] of Object.entries(config.aliases)) {
|
|
189
|
+
console.log(`${name}: ${target}`);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (subcommand === "set") {
|
|
194
|
+
const name = requirePositional(parsed.positionals[1], "Usage: mm alias set <name> <profile>");
|
|
195
|
+
const profileName = requirePositional(parsed.positionals[2], "Usage: mm alias set <name> <profile>");
|
|
196
|
+
await store.setAlias(name, profileName);
|
|
197
|
+
console.log(`alias ${name} -> ${profileName}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (subcommand === "remove") {
|
|
201
|
+
const name = requirePositional(parsed.positionals[1], "Usage: mm alias remove <name>");
|
|
202
|
+
await store.removeAlias(name);
|
|
203
|
+
console.log(`alias ${name} removed`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
throw new errors_1.ConfigError("Usage: mm alias <list|set|remove> ...");
|
|
207
|
+
}
|
|
208
|
+
default:
|
|
209
|
+
throw new errors_1.ConfigError(`Unknown command: ${command}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function handleCliError(error) {
|
|
213
|
+
if (error instanceof errors_1.MmError) {
|
|
214
|
+
console.error(`error: ${error.message}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
if (error instanceof Error) {
|
|
218
|
+
console.error(`error: ${error.message}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
console.error("error: unexpected failure");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodexClientAdapter = void 0;
|
|
4
|
+
const node_os_1 = require("node:os");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const fs_1 = require("../utils/fs");
|
|
9
|
+
const errors_1 = require("../errors");
|
|
10
|
+
function parseTopLevelToml(content) {
|
|
11
|
+
const values = {};
|
|
12
|
+
const lines = content.split(/\r?\n/);
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("[")) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const match = /^([A-Za-z0-9_\-]+)\s*=\s*"(.*)"\s*$/.exec(trimmed);
|
|
19
|
+
if (match) {
|
|
20
|
+
values[match[1]] = match[2];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return values;
|
|
24
|
+
}
|
|
25
|
+
function upsertTopLevelToml(content, values) {
|
|
26
|
+
const lines = content === "" ? [] : content.split(/\r?\n/);
|
|
27
|
+
const updatedKeys = [];
|
|
28
|
+
const pending = new Map(Object.entries(values).filter((entry) => entry[1] !== undefined));
|
|
29
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
30
|
+
const line = lines[index];
|
|
31
|
+
const match = /^([A-Za-z0-9_\-]+)\s*=/.exec(line.trim());
|
|
32
|
+
if (!match) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const key = match[1];
|
|
36
|
+
if (!pending.has(key)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
lines[index] = `${key} = ${JSON.stringify(pending.get(key))}`;
|
|
40
|
+
updatedKeys.push(key);
|
|
41
|
+
pending.delete(key);
|
|
42
|
+
}
|
|
43
|
+
const insertionIndex = lines.findIndex((line) => line.trim().startsWith("["));
|
|
44
|
+
const appended = Array.from(pending.entries()).map(([key, value]) => `${key} = ${JSON.stringify(value)}`);
|
|
45
|
+
updatedKeys.push(...Array.from(pending.keys()));
|
|
46
|
+
if (appended.length > 0) {
|
|
47
|
+
if (insertionIndex === -1) {
|
|
48
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
49
|
+
lines.push("");
|
|
50
|
+
}
|
|
51
|
+
lines.push(...appended);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const head = lines.slice(0, insertionIndex);
|
|
55
|
+
const tail = lines.slice(insertionIndex);
|
|
56
|
+
const merged = [...head];
|
|
57
|
+
if (merged.length > 0 && merged[merged.length - 1] !== "") {
|
|
58
|
+
merged.push("");
|
|
59
|
+
}
|
|
60
|
+
merged.push(...appended, "", ...tail);
|
|
61
|
+
return { content: `${merged.join("\n")}\n`, updatedKeys };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { content: `${lines.join("\n")}\n`, updatedKeys };
|
|
65
|
+
}
|
|
66
|
+
class CodexClientAdapter {
|
|
67
|
+
name = "codex";
|
|
68
|
+
getConfigPath() {
|
|
69
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), ".codex", "config.toml");
|
|
70
|
+
}
|
|
71
|
+
async readState() {
|
|
72
|
+
const configPath = this.getConfigPath();
|
|
73
|
+
const content = (0, node_fs_1.existsSync)(configPath) ? await (0, promises_1.readFile)(configPath, "utf8") : "";
|
|
74
|
+
return {
|
|
75
|
+
client: this.name,
|
|
76
|
+
configPath,
|
|
77
|
+
values: parseTopLevelToml(content)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
validateProfile(profile) {
|
|
81
|
+
const issues = [];
|
|
82
|
+
if (!profile.model) {
|
|
83
|
+
issues.push({ level: "error", message: "profile.model is required for Codex." });
|
|
84
|
+
}
|
|
85
|
+
if (!profile.baseURL) {
|
|
86
|
+
issues.push({ level: "warn", message: "profile.baseURL is unset; Codex may continue using the previous endpoint." });
|
|
87
|
+
}
|
|
88
|
+
if (!profile.apiKey && !profile.apiKeyEnv) {
|
|
89
|
+
issues.push({ level: "warn", message: "Neither apiKey nor apiKeyEnv is set; authentication may fail." });
|
|
90
|
+
}
|
|
91
|
+
return issues;
|
|
92
|
+
}
|
|
93
|
+
compareProfile(state, profile) {
|
|
94
|
+
const issues = [];
|
|
95
|
+
const expected = {
|
|
96
|
+
provider: profile.provider,
|
|
97
|
+
model: profile.model,
|
|
98
|
+
base_url: profile.baseURL,
|
|
99
|
+
api_key: profile.apiKey,
|
|
100
|
+
api_key_env: profile.apiKeyEnv,
|
|
101
|
+
model_reasoning_effort: profile.reasoningEffort
|
|
102
|
+
};
|
|
103
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const actual = state.values[key];
|
|
108
|
+
if (actual !== value) {
|
|
109
|
+
issues.push({
|
|
110
|
+
level: "warn",
|
|
111
|
+
message: `${key} drifted: codex has ${actual ?? "(unset)"}, profile wants ${value}.`
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (issues.length === 0) {
|
|
116
|
+
issues.push({ level: "info", message: "Codex config matches the selected profile." });
|
|
117
|
+
}
|
|
118
|
+
return issues;
|
|
119
|
+
}
|
|
120
|
+
async applyProfile(profileName, profile) {
|
|
121
|
+
const configPath = this.getConfigPath();
|
|
122
|
+
await (0, fs_1.ensureDirForFile)(configPath);
|
|
123
|
+
const backupPath = await (0, fs_1.backupFile)(configPath);
|
|
124
|
+
const currentContent = (0, node_fs_1.existsSync)(configPath) ? await (0, promises_1.readFile)(configPath, "utf8") : "";
|
|
125
|
+
const { content, updatedKeys } = upsertTopLevelToml(currentContent, {
|
|
126
|
+
provider: profile.provider,
|
|
127
|
+
model: profile.model,
|
|
128
|
+
base_url: profile.baseURL,
|
|
129
|
+
api_key: profile.apiKey,
|
|
130
|
+
api_key_env: profile.apiKeyEnv,
|
|
131
|
+
model_reasoning_effort: profile.reasoningEffort
|
|
132
|
+
});
|
|
133
|
+
await (0, promises_1.writeFile)(configPath, content, "utf8");
|
|
134
|
+
return {
|
|
135
|
+
client: this.name,
|
|
136
|
+
configPath,
|
|
137
|
+
updatedKeys,
|
|
138
|
+
backupPath
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async rollback() {
|
|
142
|
+
const configPath = this.getConfigPath();
|
|
143
|
+
const backupPath = await (0, fs_1.restoreBackupFile)(configPath);
|
|
144
|
+
if (!backupPath) {
|
|
145
|
+
throw new errors_1.ConfigError(`No backup found for ${configPath}.`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
client: this.name,
|
|
149
|
+
configPath,
|
|
150
|
+
backupPath
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.CodexClientAdapter = CodexClientAdapter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getClientAdapter = getClientAdapter;
|
|
4
|
+
exports.listClientNames = listClientNames;
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
const codex_1 = require("./codex");
|
|
7
|
+
const REGISTRY = {
|
|
8
|
+
codex: new codex_1.CodexClientAdapter()
|
|
9
|
+
};
|
|
10
|
+
function getClientAdapter(name) {
|
|
11
|
+
const adapter = REGISTRY[name];
|
|
12
|
+
if (!adapter) {
|
|
13
|
+
throw new errors_1.ClientNotFoundError(`Client adapter not found for ${name}.`);
|
|
14
|
+
}
|
|
15
|
+
return adapter;
|
|
16
|
+
}
|
|
17
|
+
function listClientNames() {
|
|
18
|
+
return Object.keys(REGISTRY);
|
|
19
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigStore = void 0;
|
|
4
|
+
const node_process_1 = require("node:process");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const fs_1 = require("../utils/fs");
|
|
8
|
+
const DEFAULT_GLOBAL_CONFIG = {
|
|
9
|
+
defaultClient: "codex",
|
|
10
|
+
currentProfiles: {},
|
|
11
|
+
profiles: {},
|
|
12
|
+
aliases: {}
|
|
13
|
+
};
|
|
14
|
+
class ConfigStore {
|
|
15
|
+
getGlobalConfigPath() {
|
|
16
|
+
if (process.platform === "win32" && process.env.APPDATA) {
|
|
17
|
+
return (0, node_path_1.join)(process.env.APPDATA, "mm", "config.json");
|
|
18
|
+
}
|
|
19
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "mm", "config.json");
|
|
20
|
+
}
|
|
21
|
+
getProjectConfigPath(startDir = (0, node_process_1.cwd)()) {
|
|
22
|
+
const found = (0, fs_1.findUp)(".mm.json", startDir);
|
|
23
|
+
return found ?? (0, node_path_1.join)(startDir, ".mm.json");
|
|
24
|
+
}
|
|
25
|
+
async readGlobalConfig() {
|
|
26
|
+
const config = await (0, fs_1.readJsonFile)(this.getGlobalConfigPath());
|
|
27
|
+
return this.mergeGlobalConfig(config);
|
|
28
|
+
}
|
|
29
|
+
async writeGlobalConfig(config) {
|
|
30
|
+
await (0, fs_1.writeJsonFile)(this.getGlobalConfigPath(), config);
|
|
31
|
+
}
|
|
32
|
+
async readProjectConfig(startDir = (0, node_process_1.cwd)()) {
|
|
33
|
+
return (0, fs_1.readJsonFile)(this.getProjectConfigPath(startDir));
|
|
34
|
+
}
|
|
35
|
+
async writeProjectConfig(config, startDir = (0, node_process_1.cwd)()) {
|
|
36
|
+
await (0, fs_1.writeJsonFile)(this.getProjectConfigPath(startDir), config);
|
|
37
|
+
}
|
|
38
|
+
async setCurrentProfile(client, profileName, scope) {
|
|
39
|
+
if (scope === "project") {
|
|
40
|
+
const projectConfig = (await this.readProjectConfig()) ?? {};
|
|
41
|
+
projectConfig.profile = profileName;
|
|
42
|
+
await this.writeProjectConfig(projectConfig);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const globalConfig = await this.readGlobalConfig();
|
|
46
|
+
globalConfig.currentProfiles[client] = profileName;
|
|
47
|
+
await this.writeGlobalConfig(globalConfig);
|
|
48
|
+
}
|
|
49
|
+
async upsertProfile(name, profile) {
|
|
50
|
+
const globalConfig = await this.readGlobalConfig();
|
|
51
|
+
globalConfig.profiles[name] = {
|
|
52
|
+
...globalConfig.profiles[name],
|
|
53
|
+
...profile
|
|
54
|
+
};
|
|
55
|
+
await this.writeGlobalConfig(globalConfig);
|
|
56
|
+
}
|
|
57
|
+
async removeProfile(name) {
|
|
58
|
+
const globalConfig = await this.readGlobalConfig();
|
|
59
|
+
delete globalConfig.profiles[name];
|
|
60
|
+
for (const client of Object.keys(globalConfig.currentProfiles)) {
|
|
61
|
+
if (globalConfig.currentProfiles[client] === name) {
|
|
62
|
+
delete globalConfig.currentProfiles[client];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
delete globalConfig.aliases[name];
|
|
66
|
+
await this.writeGlobalConfig(globalConfig);
|
|
67
|
+
}
|
|
68
|
+
async setAlias(name, profileName) {
|
|
69
|
+
const globalConfig = await this.readGlobalConfig();
|
|
70
|
+
globalConfig.aliases[name] = profileName;
|
|
71
|
+
await this.writeGlobalConfig(globalConfig);
|
|
72
|
+
}
|
|
73
|
+
async removeAlias(name) {
|
|
74
|
+
const globalConfig = await this.readGlobalConfig();
|
|
75
|
+
delete globalConfig.aliases[name];
|
|
76
|
+
await this.writeGlobalConfig(globalConfig);
|
|
77
|
+
}
|
|
78
|
+
async setConfigValue(key, value) {
|
|
79
|
+
const globalConfig = await this.readGlobalConfig();
|
|
80
|
+
const path = key.split(".");
|
|
81
|
+
let cursor = globalConfig;
|
|
82
|
+
for (const segment of path.slice(0, -1)) {
|
|
83
|
+
const next = cursor[segment];
|
|
84
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
85
|
+
cursor[segment] = {};
|
|
86
|
+
}
|
|
87
|
+
cursor = cursor[segment];
|
|
88
|
+
}
|
|
89
|
+
cursor[path[path.length - 1]] = value;
|
|
90
|
+
await this.writeGlobalConfig(globalConfig);
|
|
91
|
+
}
|
|
92
|
+
async getConfigValue(key) {
|
|
93
|
+
const globalConfig = await this.readGlobalConfig();
|
|
94
|
+
return key.split(".").reduce((acc, segment) => {
|
|
95
|
+
if (!acc || typeof acc !== "object") {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return acc[segment];
|
|
99
|
+
}, globalConfig);
|
|
100
|
+
}
|
|
101
|
+
mergeGlobalConfig(config) {
|
|
102
|
+
return {
|
|
103
|
+
defaultClient: config?.defaultClient ?? DEFAULT_GLOBAL_CONFIG.defaultClient,
|
|
104
|
+
currentProfiles: {
|
|
105
|
+
...DEFAULT_GLOBAL_CONFIG.currentProfiles,
|
|
106
|
+
...(config?.currentProfiles ?? {})
|
|
107
|
+
},
|
|
108
|
+
profiles: {
|
|
109
|
+
...DEFAULT_GLOBAL_CONFIG.profiles,
|
|
110
|
+
...(config?.profiles ?? {})
|
|
111
|
+
},
|
|
112
|
+
aliases: {
|
|
113
|
+
...DEFAULT_GLOBAL_CONFIG.aliases,
|
|
114
|
+
...(config?.aliases ?? {})
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.ConfigStore = ConfigStore;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Resolver = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
class Resolver {
|
|
6
|
+
store;
|
|
7
|
+
constructor(store) {
|
|
8
|
+
this.store = store;
|
|
9
|
+
}
|
|
10
|
+
async resolveProfile(options) {
|
|
11
|
+
const globalConfig = await this.store.readGlobalConfig();
|
|
12
|
+
const projectConfig = await this.store.readProjectConfig();
|
|
13
|
+
const profileField = this.pickFirst([
|
|
14
|
+
{ value: options.profileOverride, source: "command line" },
|
|
15
|
+
{ value: projectConfig?.profile, source: "project config" },
|
|
16
|
+
{ value: process.env.MM_PROFILE, source: "environment" },
|
|
17
|
+
{ value: globalConfig.currentProfiles[options.client], source: "global config" }
|
|
18
|
+
]);
|
|
19
|
+
if (!profileField.value) {
|
|
20
|
+
throw new errors_1.ConfigError(`No active profile for client ${options.client}. Use \`mm use <profile>\` first.`);
|
|
21
|
+
}
|
|
22
|
+
const expandedProfileName = globalConfig.aliases[profileField.value] ?? profileField.value;
|
|
23
|
+
const profile = globalConfig.profiles[expandedProfileName];
|
|
24
|
+
if (!profile) {
|
|
25
|
+
throw new errors_1.ProfileNotFoundError(`Unknown profile: ${expandedProfileName}`);
|
|
26
|
+
}
|
|
27
|
+
const profileSource = globalConfig.aliases[profileField.value] && expandedProfileName !== profileField.value
|
|
28
|
+
? `${profileField.source} via alias`
|
|
29
|
+
: profileField.source;
|
|
30
|
+
return {
|
|
31
|
+
client: options.client,
|
|
32
|
+
profileName: expandedProfileName,
|
|
33
|
+
profile,
|
|
34
|
+
resolvedFrom: {
|
|
35
|
+
profileName: profileSource
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
pickFirst(values) {
|
|
40
|
+
for (const item of values) {
|
|
41
|
+
if (item.value !== undefined && item.value !== "") {
|
|
42
|
+
return item;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { value: undefined, source: "unset" };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.Resolver = Resolver;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClientNotFoundError = exports.ProfileNotFoundError = exports.ConfigError = exports.MmError = void 0;
|
|
4
|
+
class MmError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.MmError = MmError;
|
|
11
|
+
class ConfigError extends MmError {
|
|
12
|
+
}
|
|
13
|
+
exports.ConfigError = ConfigError;
|
|
14
|
+
class ProfileNotFoundError extends MmError {
|
|
15
|
+
}
|
|
16
|
+
exports.ProfileNotFoundError = ProfileNotFoundError;
|
|
17
|
+
class ClientNotFoundError extends MmError {
|
|
18
|
+
}
|
|
19
|
+
exports.ClientNotFoundError = ClientNotFoundError;
|
package/dist/index.js
ADDED
package/dist/output.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.printConfig = printConfig;
|
|
4
|
+
exports.printList = printList;
|
|
5
|
+
exports.printCurrent = printCurrent;
|
|
6
|
+
exports.printWhich = printWhich;
|
|
7
|
+
exports.printApplyResult = printApplyResult;
|
|
8
|
+
exports.printRollbackResult = printRollbackResult;
|
|
9
|
+
exports.printIssues = printIssues;
|
|
10
|
+
function printConfig(config) {
|
|
11
|
+
console.log(JSON.stringify(config, null, 2));
|
|
12
|
+
}
|
|
13
|
+
function printList(config) {
|
|
14
|
+
console.log("clients:");
|
|
15
|
+
for (const [client, profile] of Object.entries(config.currentProfiles)) {
|
|
16
|
+
console.log(` ${client}: ${profile}`);
|
|
17
|
+
}
|
|
18
|
+
console.log("profiles:");
|
|
19
|
+
for (const [name, profile] of Object.entries(config.profiles)) {
|
|
20
|
+
const summary = [profile.provider, profile.model].filter(Boolean).join(" / ");
|
|
21
|
+
console.log(` ${name}${summary ? ` -> ${summary}` : ""}`);
|
|
22
|
+
}
|
|
23
|
+
console.log("aliases:");
|
|
24
|
+
for (const [name, target] of Object.entries(config.aliases)) {
|
|
25
|
+
console.log(` ${name} -> ${target}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function printCurrent(resolved, state) {
|
|
29
|
+
console.log(`client: ${resolved.client}`);
|
|
30
|
+
console.log(`profile: ${resolved.profileName}`);
|
|
31
|
+
console.log(`provider: ${resolved.profile.provider ?? "(unset)"}`);
|
|
32
|
+
console.log(`model: ${resolved.profile.model ?? "(unset)"}`);
|
|
33
|
+
console.log(`baseURL: ${resolved.profile.baseURL ?? "(unset)"}`);
|
|
34
|
+
console.log(`configPath: ${state.configPath}`);
|
|
35
|
+
console.log(`source: ${resolved.resolvedFrom.profileName}`);
|
|
36
|
+
}
|
|
37
|
+
function printWhich(resolved, state) {
|
|
38
|
+
console.log(`client: ${resolved.client}`);
|
|
39
|
+
console.log(`profile: ${resolved.profileName}`);
|
|
40
|
+
console.log(`configPath: ${state.configPath}`);
|
|
41
|
+
console.log(`resolvedFrom.profile: ${resolved.resolvedFrom.profileName}`);
|
|
42
|
+
console.log(`profile.provider: ${resolved.profile.provider ?? "(unset)"}`);
|
|
43
|
+
console.log(`profile.model: ${resolved.profile.model ?? "(unset)"}`);
|
|
44
|
+
console.log(`profile.baseURL: ${resolved.profile.baseURL ?? "(unset)"}`);
|
|
45
|
+
console.log(`profile.apiKey: ${resolved.profile.apiKey ? "(set)" : "(unset)"}`);
|
|
46
|
+
console.log(`profile.apiKeyEnv: ${resolved.profile.apiKeyEnv ?? "(unset)"}`);
|
|
47
|
+
console.log(`codex.provider: ${state.values.provider ?? "(unset)"}`);
|
|
48
|
+
console.log(`codex.model: ${state.values.model ?? "(unset)"}`);
|
|
49
|
+
console.log(`codex.base_url: ${state.values.base_url ?? "(unset)"}`);
|
|
50
|
+
console.log(`codex.api_key: ${state.values.api_key ? "(set)" : "(unset)"}`);
|
|
51
|
+
console.log(`codex.api_key_env: ${state.values.api_key_env ?? "(unset)"}`);
|
|
52
|
+
}
|
|
53
|
+
function printApplyResult(result) {
|
|
54
|
+
console.log(`client: ${result.client}`);
|
|
55
|
+
console.log(`configPath: ${result.configPath}`);
|
|
56
|
+
console.log(`updatedKeys: ${result.updatedKeys.join(", ") || "(none)"}`);
|
|
57
|
+
if (result.backupPath) {
|
|
58
|
+
console.log(`backup: ${result.backupPath}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function printRollbackResult(result) {
|
|
62
|
+
console.log(`client: ${result.client}`);
|
|
63
|
+
console.log(`configPath: ${result.configPath}`);
|
|
64
|
+
console.log(`restoredFrom: ${result.backupPath}`);
|
|
65
|
+
}
|
|
66
|
+
function printIssues(issues) {
|
|
67
|
+
if (issues.length === 0) {
|
|
68
|
+
console.log("ok: no issues found");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const issue of issues) {
|
|
72
|
+
console.log(`${issue.level}: ${issue.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/types.js
ADDED
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readJsonFile = readJsonFile;
|
|
4
|
+
exports.writeJsonFile = writeJsonFile;
|
|
5
|
+
exports.ensureDirForFile = ensureDirForFile;
|
|
6
|
+
exports.backupFile = backupFile;
|
|
7
|
+
exports.restoreBackupFile = restoreBackupFile;
|
|
8
|
+
exports.findUp = findUp;
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const promises_1 = require("node:fs/promises");
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
async function readJsonFile(filePath) {
|
|
13
|
+
if (!(0, node_fs_1.existsSync)(filePath)) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const content = await (0, promises_1.readFile)(filePath, "utf8");
|
|
17
|
+
return JSON.parse(content);
|
|
18
|
+
}
|
|
19
|
+
async function writeJsonFile(filePath, value) {
|
|
20
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(filePath), { recursive: true });
|
|
21
|
+
await (0, promises_1.writeFile)(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
22
|
+
}
|
|
23
|
+
async function ensureDirForFile(filePath) {
|
|
24
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(filePath), { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
async function backupFile(filePath) {
|
|
27
|
+
if (!(0, node_fs_1.existsSync)(filePath)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const backupPath = `${filePath}.bak`;
|
|
31
|
+
await (0, promises_1.copyFile)(filePath, backupPath);
|
|
32
|
+
return backupPath;
|
|
33
|
+
}
|
|
34
|
+
async function restoreBackupFile(filePath) {
|
|
35
|
+
const backupPath = `${filePath}.bak`;
|
|
36
|
+
if (!(0, node_fs_1.existsSync)(backupPath)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
await (0, promises_1.rename)(backupPath, filePath);
|
|
40
|
+
return backupPath;
|
|
41
|
+
}
|
|
42
|
+
function findUp(fileName, startDir) {
|
|
43
|
+
let current = startDir;
|
|
44
|
+
while (true) {
|
|
45
|
+
const candidate = (0, node_path_1.join)(current, fileName);
|
|
46
|
+
if ((0, node_fs_1.existsSync)(candidate)) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
const parent = (0, node_path_1.dirname)(current);
|
|
50
|
+
if (parent === current || current === (0, node_path_1.parse)(current).root) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
current = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orionpotter/menv",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/OrionPotter/menv"
|
|
7
|
+
},
|
|
8
|
+
"description": "CLI profile manager for Codex and other AI CLIs",
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"bin": {
|
|
11
|
+
"mm": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
20
|
+
"prepare": "npm run build",
|
|
21
|
+
"prepublishOnly": "npm run check && npm run build",
|
|
22
|
+
"start": "node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"cli",
|
|
26
|
+
"codex",
|
|
27
|
+
"config",
|
|
28
|
+
"provider-manager",
|
|
29
|
+
"npm"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.6.0",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|