@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 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
+ }