@glidea/codex-switch 0.1.1
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.en.md +96 -0
- package/README.md +100 -0
- package/bin/codex-switch.mjs +232 -0
- package/package.json +22 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# codex-switch
|
|
2
|
+
|
|
3
|
+
A tiny CLI tool for one job only
|
|
4
|
+
Switch Codex `config.toml` and `auth.json` in one command
|
|
5
|
+
|
|
6
|
+
Highlights
|
|
7
|
+
- Simple: only a few commands
|
|
8
|
+
- Lightweight: no runtime dependencies
|
|
9
|
+
- Transparent: it only manages two files
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @glidea/codex-switch
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
You can also run it without installing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @glidea/codex-switch list
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
1. Save your current setup as `openai`
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
codex-switch add openai --from-current
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. Create another setup `glidea`
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
codex-switch add glidea
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It will open `config.toml` first
|
|
38
|
+
Then it will open `auth.json`
|
|
39
|
+
|
|
40
|
+
3. Switch any time
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
codex-switch openai
|
|
44
|
+
codex-switch glidea
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
4. Restart Codex after switching
|
|
48
|
+
|
|
49
|
+
An already running Codex session does not hot-reload config files
|
|
50
|
+
Restart is required to load the new profile
|
|
51
|
+
|
|
52
|
+
## Common Commands
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
codex-switch add <profile>
|
|
56
|
+
codex-switch add <profile> --from-current
|
|
57
|
+
codex-switch add <profile> --config <path> --auth <path>
|
|
58
|
+
codex-switch edit <profile>
|
|
59
|
+
codex-switch <profile>
|
|
60
|
+
codex-switch <profile> --copy
|
|
61
|
+
codex-switch list
|
|
62
|
+
codex-switch current
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## File Layout
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
~/.codex/
|
|
69
|
+
config.toml -> profiles/openai/config.toml
|
|
70
|
+
auth.json -> profiles/openai/auth.json
|
|
71
|
+
profiles/
|
|
72
|
+
openai/
|
|
73
|
+
config.toml
|
|
74
|
+
auth.json
|
|
75
|
+
glidea/
|
|
76
|
+
config.toml
|
|
77
|
+
auth.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Default mode is symlink mode:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
codex-switch <profile>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If symlink permission fails on Windows, use copy mode:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
codex-switch <profile> --copy
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Release
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
./scripts/publish-with-token.sh <NPM_TOKEN>
|
|
96
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# codex-switch
|
|
2
|
+
|
|
3
|
+
English README: [README.en.md](./README.en.md)
|
|
4
|
+
|
|
5
|
+
一个超轻量命令行工具
|
|
6
|
+
只做一件事
|
|
7
|
+
一键切换 Codex 的 `config.toml` 和 `auth.json`
|
|
8
|
+
|
|
9
|
+
特点
|
|
10
|
+
- 简单:核心命令只有几个
|
|
11
|
+
- 轻量:无运行时依赖
|
|
12
|
+
- 透明:本质就是管理两个文件
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @glidea/codex-switch
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
不想安装也可以直接用
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @glidea/codex-switch list
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 快速上手
|
|
27
|
+
|
|
28
|
+
1. 把你当前官方订阅配置保存成 `openai`
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
codex-switch add openai --from-current
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
2. 新建另一个配置 `glidea`
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
codex-switch add glidea
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
执行后会先打开 `config.toml` 编辑
|
|
41
|
+
再打开 `auth.json` 编辑
|
|
42
|
+
|
|
43
|
+
3. 随时切换
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
codex-switch openai
|
|
47
|
+
codex-switch glidea
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
4. 切换后重启 Codex
|
|
51
|
+
|
|
52
|
+
已运行的 Codex 会话不会热更新配置
|
|
53
|
+
重启后才会读取新配置
|
|
54
|
+
|
|
55
|
+
## 常用命令
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
codex-switch add <profile>
|
|
59
|
+
codex-switch add <profile> --from-current
|
|
60
|
+
codex-switch add <profile> --config <path> --auth <path>
|
|
61
|
+
codex-switch edit <profile>
|
|
62
|
+
codex-switch <profile>
|
|
63
|
+
codex-switch <profile> --copy
|
|
64
|
+
codex-switch list
|
|
65
|
+
codex-switch current
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 文件结构
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
~/.codex/
|
|
72
|
+
config.toml -> profiles/openai/config.toml
|
|
73
|
+
auth.json -> profiles/openai/auth.json
|
|
74
|
+
profiles/
|
|
75
|
+
openai/
|
|
76
|
+
config.toml
|
|
77
|
+
auth.json
|
|
78
|
+
glidea/
|
|
79
|
+
config.toml
|
|
80
|
+
auth.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
默认是软链接模式
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
codex-switch <profile>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
如果你在 Windows 上遇到软链接权限问题
|
|
90
|
+
用复制模式
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
codex-switch <profile> --copy
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 发布
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
./scripts/publish-with-token.sh <NPM_TOKEN>
|
|
100
|
+
```
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { spawnSync } from "node:child_process"
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2)
|
|
9
|
+
const codexDir = path.join(os.homedir(), ".codex")
|
|
10
|
+
const profilesDir = path.join(codexDir, "profiles")
|
|
11
|
+
|
|
12
|
+
function fail(message) {
|
|
13
|
+
console.error(message)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function usage() {
|
|
18
|
+
console.log("usage: codex-switch <profile>|add <profile>|edit <profile>|list|current [--copy]")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function profilePaths(profileName) {
|
|
22
|
+
const dirPath = path.join(profilesDir, profileName)
|
|
23
|
+
return {
|
|
24
|
+
dirPath,
|
|
25
|
+
configPath: path.join(dirPath, "config.toml"),
|
|
26
|
+
authPath: path.join(dirPath, "auth.json")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ensureProfileExists(profileName) {
|
|
31
|
+
const paths = profilePaths(profileName)
|
|
32
|
+
if (!fs.existsSync(paths.configPath) || !fs.existsSync(paths.authPath)) {
|
|
33
|
+
fail(`profile not found: ${profileName}`)
|
|
34
|
+
}
|
|
35
|
+
return paths
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function switchProfile(profileName, useCopy) {
|
|
39
|
+
const paths = ensureProfileExists(profileName)
|
|
40
|
+
fs.mkdirSync(codexDir, { recursive: true })
|
|
41
|
+
|
|
42
|
+
const rootConfigPath = path.join(codexDir, "config.toml")
|
|
43
|
+
const rootAuthPath = path.join(codexDir, "auth.json")
|
|
44
|
+
|
|
45
|
+
fs.rmSync(rootConfigPath, { force: true })
|
|
46
|
+
fs.rmSync(rootAuthPath, { force: true })
|
|
47
|
+
|
|
48
|
+
if (useCopy) {
|
|
49
|
+
fs.copyFileSync(paths.configPath, rootConfigPath)
|
|
50
|
+
fs.copyFileSync(paths.authPath, rootAuthPath)
|
|
51
|
+
} else {
|
|
52
|
+
fs.symlinkSync(paths.configPath, rootConfigPath, "file")
|
|
53
|
+
fs.symlinkSync(paths.authPath, rootAuthPath, "file")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getCurrentProfileName() {
|
|
58
|
+
const rootConfigPath = path.join(codexDir, "config.toml")
|
|
59
|
+
if (!fs.existsSync(rootConfigPath)) {
|
|
60
|
+
fail("current profile not found")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const stat = fs.lstatSync(rootConfigPath)
|
|
64
|
+
if (!stat.isSymbolicLink()) {
|
|
65
|
+
fail("current profile is not symlink")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const linkTarget = fs.readlinkSync(rootConfigPath)
|
|
69
|
+
const fullTarget = path.isAbsolute(linkTarget)
|
|
70
|
+
? linkTarget
|
|
71
|
+
: path.resolve(path.dirname(rootConfigPath), linkTarget)
|
|
72
|
+
const parts = fullTarget.split(path.sep)
|
|
73
|
+
const profilesIndex = parts.lastIndexOf("profiles")
|
|
74
|
+
if (profilesIndex < 0 || profilesIndex + 1 >= parts.length) {
|
|
75
|
+
fail("cannot parse current profile")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parts[profilesIndex + 1]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function listProfiles() {
|
|
82
|
+
if (!fs.existsSync(profilesDir)) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
const names = fs
|
|
86
|
+
.readdirSync(profilesDir, { withFileTypes: true })
|
|
87
|
+
.filter((entry) => entry.isDirectory())
|
|
88
|
+
.map((entry) => entry.name)
|
|
89
|
+
.sort()
|
|
90
|
+
if (names.length > 0) {
|
|
91
|
+
console.log(names.join("\n"))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseAddOptions(addArgs) {
|
|
96
|
+
let fromCurrent = false
|
|
97
|
+
let configSourcePath = ""
|
|
98
|
+
let authSourcePath = ""
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < addArgs.length; i += 1) {
|
|
101
|
+
const token = addArgs[i]
|
|
102
|
+
if (token === "--from-current") {
|
|
103
|
+
fromCurrent = true
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (token === "--config") {
|
|
107
|
+
configSourcePath = addArgs[i + 1] || ""
|
|
108
|
+
i += 1
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if (token === "--auth") {
|
|
112
|
+
authSourcePath = addArgs[i + 1] || ""
|
|
113
|
+
i += 1
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
fail(`unknown option: ${token}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (fromCurrent && (configSourcePath || authSourcePath)) {
|
|
120
|
+
fail("cannot mix --from-current with --config/--auth")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if ((configSourcePath && !authSourcePath) || (!configSourcePath && authSourcePath)) {
|
|
124
|
+
fail("both --config and --auth are required")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
fromCurrent,
|
|
129
|
+
configSourcePath,
|
|
130
|
+
authSourcePath
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function editFile(filePath) {
|
|
135
|
+
fs.writeFileSync(filePath, "")
|
|
136
|
+
const editor = process.env.EDITOR || "vi"
|
|
137
|
+
const result = spawnSync(editor, [filePath], { stdio: "inherit" })
|
|
138
|
+
if (result.status !== 0) {
|
|
139
|
+
fail(`editor failed: ${editor}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function addProfile(profileName, optionArgs) {
|
|
144
|
+
const options = parseAddOptions(optionArgs)
|
|
145
|
+
const paths = profilePaths(profileName)
|
|
146
|
+
if (fs.existsSync(paths.dirPath)) {
|
|
147
|
+
fail(`profile already exists: ${profileName}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fs.mkdirSync(paths.dirPath, { recursive: true })
|
|
151
|
+
|
|
152
|
+
if (options.fromCurrent) {
|
|
153
|
+
fs.copyFileSync(path.join(codexDir, "config.toml"), paths.configPath)
|
|
154
|
+
fs.copyFileSync(path.join(codexDir, "auth.json"), paths.authPath)
|
|
155
|
+
} else if (options.configSourcePath) {
|
|
156
|
+
fs.copyFileSync(options.configSourcePath, paths.configPath)
|
|
157
|
+
fs.copyFileSync(options.authSourcePath, paths.authPath)
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`edit config.toml: ${paths.configPath}`)
|
|
160
|
+
editFile(paths.configPath)
|
|
161
|
+
console.log(`edit auth.json: ${paths.authPath}`)
|
|
162
|
+
editFile(paths.authPath)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
switchProfile(profileName, false)
|
|
166
|
+
console.log(`switched to ${profileName}`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function editProfile(profileName) {
|
|
170
|
+
const paths = ensureProfileExists(profileName)
|
|
171
|
+
console.log(`edit config.toml: ${paths.configPath}`)
|
|
172
|
+
editFile(paths.configPath)
|
|
173
|
+
console.log(`edit auth.json: ${paths.authPath}`)
|
|
174
|
+
editFile(paths.authPath)
|
|
175
|
+
switchProfile(profileName, false)
|
|
176
|
+
console.log(`switched to ${profileName}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function run() {
|
|
180
|
+
if (args.length === 0) {
|
|
181
|
+
usage()
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const command = args[0]
|
|
186
|
+
|
|
187
|
+
if (command === "list") {
|
|
188
|
+
listProfiles()
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (command === "current") {
|
|
193
|
+
console.log(getCurrentProfileName())
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (command === "add") {
|
|
198
|
+
const profileName = args[1]
|
|
199
|
+
if (!profileName) {
|
|
200
|
+
usage()
|
|
201
|
+
process.exit(1)
|
|
202
|
+
}
|
|
203
|
+
addProfile(profileName, args.slice(2))
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (command === "edit") {
|
|
208
|
+
const profileName = args[1]
|
|
209
|
+
if (!profileName || args.length !== 2) {
|
|
210
|
+
usage()
|
|
211
|
+
process.exit(1)
|
|
212
|
+
}
|
|
213
|
+
editProfile(profileName)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const profileName = command
|
|
218
|
+
let useCopy = false
|
|
219
|
+
if (args.length > 1) {
|
|
220
|
+
if (args.length === 2 && args[1] === "--copy") {
|
|
221
|
+
useCopy = true
|
|
222
|
+
} else {
|
|
223
|
+
usage()
|
|
224
|
+
process.exit(1)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
switchProfile(profileName, useCopy)
|
|
229
|
+
console.log(`switched to ${profileName}`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
run()
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glidea/codex-switch",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Switch Codex profiles quickly",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"bin",
|
|
9
|
+
"README.md",
|
|
10
|
+
"README.en.md"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"codex-switch": "./bin/codex-switch.mjs"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test",
|
|
17
|
+
"prepublishOnly": "npm test"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
}
|
|
22
|
+
}
|