base_parts_ai 1.0.55 → 1.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
-
1
+
2
2
  ## npx 运行
3
3
 
4
4
  已发布到 npm 仓库后,可以直接运行入口命令:
@@ -7,7 +7,7 @@
7
7
  npx base_parts_ai
8
8
  ```
9
9
 
10
- 无参数时会让用户选择 `jcc`、`jcx` 或 `weds`,再继续执行对应配置流程。
10
+ 无参数时会让用户选择 `jcc`、`jcx`、`weds`、`ucx` 或 `jgrok`,再继续执行对应流程。
11
11
 
12
12
  也可以明确指定工具,适合脚本或需要带参数的场景:
13
13
 
@@ -15,6 +15,8 @@ npx base_parts_ai
15
15
  npx base_parts_ai jcc
16
16
  npx base_parts_ai jcx
17
17
  npx base_parts_ai weds
18
+ npx base_parts_ai ucx
19
+ npx base_parts_ai jgrok
18
20
  npx base_parts_ai jcc setapi
19
21
  npx base_parts_ai jcx setapi
20
22
  npx base_parts_ai weds setapi
@@ -27,18 +29,29 @@ npx base_parts_ai --help
27
29
  npx base_parts_ai --version
28
30
  ```
29
31
 
30
- 通过 `base_parts_ai` 入口执行时,会屏蔽 `base_parts_ai` 自身自动升级;`jcc`、`jcx`、`weds` 单独运行行为保持不变。
32
+ 通过 `base_parts_ai` 入口执行时,会屏蔽 `base_parts_ai` 自身自动升级;`jcc`、`jcx`、`weds`、`ucx`、`jgrok` 单独运行行为保持不变。
33
+
34
+ 全局安装后也可以直接运行:
35
+
36
+ ```powershell
37
+ ucx
38
+ jgrok
39
+ ```
40
+
41
+ `ucx` 会扫描用户主目录下一层的 `~/.codex` 与 `~/.codex_*`。账号名优先用 `auth.json` 中非空 `OPENAI_API_KEY` 的脱敏短名,否则用 JWT email;不会自动创建空的 `~/.codex`。
42
+
43
+ `jgrok` 若 `~/.grok/.env` 不存在则写入代理示例并打印绝对路径,请编辑后再运行。已存在则把该文件注入环境变量:本机没有 `grok` 时通过腾讯云 npm 源安装 `@xai-official/grok`(安装不注入环境变量),然后启动 `grok`,用户参数原样透传。
31
44
 
32
45
  ## 下载发布的包
33
- https://registry.npmjs.org/base_parts_ai/-/base_parts_ai-1.0.45.tgz
34
-
35
- ## 然后放在jdwfiles目录下
36
- https://jdwfiles.oss-cn-hangzhou.aliyuncs.com/npm_pkg/base_parts_ai-1.0.34.tgz
37
-
38
-
39
- # 2.1.34
40
- User-Agent: claude-cli/2.1.34 (external, cli)
41
- claude-opus-4-6
42
- claude-sonnet-4-6
43
- claude-haiku-4-5-20251001
44
-
46
+ https://registry.npmjs.org/base_parts_ai/-/base_parts_ai-1.0.57.tgz
47
+
48
+ ## 然后放在jdwfiles目录下
49
+ https://jdwfiles.oss-cn-hangzhou.aliyuncs.com/npm_pkg/base_parts_ai-1.0.34.tgz
50
+
51
+
52
+ # 2.1.34
53
+ User-Agent: claude-cli/2.1.34 (external, cli)
54
+ claude-opus-4-6
55
+ claude-sonnet-4-6
56
+ claude-haiku-4-5-20251001
57
+
package/bin/jgrok.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ var os = require('os');
6
+ var path = require('path');
7
+
8
+ /**
9
+ * 构建 jgrok 运行配置,只解析主目录
10
+ * @returns {{ homeDir: string }}
11
+ */
12
+ function getBuildCfg() {
13
+ var homeDir = process.env.JCC_TEST_HOME || os.homedir();
14
+ return { homeDir: homeDir };
15
+ }
16
+
17
+ /**
18
+ * 动态加载 lib 下的命令模块,便于本地开发时读取最新文件
19
+ * @param {string} libName lib 目录下的文件名(不含 .js)
20
+ * @returns {Function}
21
+ */
22
+ function loadLib(libName) {
23
+ var libPath = path.resolve(__dirname, '../lib/' + libName + '.js');
24
+ delete require.cache[require.resolve(libPath)];
25
+ return require(libPath);
26
+ }
27
+
28
+ /**
29
+ * 运行 jgrok CLI,参数全部透传给 grok,可供命令行直接执行或被 base_parts_ai require 调用
30
+ * @param {Array<string>} args 用户参数,不包含 node 和脚本路径
31
+ * @returns {Promise<number>}
32
+ */
33
+ async function runCli(args) {
34
+ var buildCfg = getBuildCfg();
35
+ return await loadLib('jgrok')(args || [], buildCfg);
36
+ }
37
+
38
+ if (require.main === module) {
39
+ runCli(process.argv.slice(2)).then(function (code) {
40
+ process.exit(typeof code === 'number' ? code : 0);
41
+ }).catch(function (err) {
42
+ console.error('jgrok 执行失败:', err.message);
43
+ process.exit(1);
44
+ });
45
+ }
46
+
47
+ module.exports = runCli;
@@ -16,5 +16,7 @@ console.log(c.cyan + ' ╔═════════════════
16
16
  console.log(c.cyan + ' ║ ' + c.yellow + '安装完成!输入以下命令开始配置:' + c.reset);
17
17
  console.log(c.cyan + ' ║ ' + c.green + 'jcc' + c.white + ' 配置或安装 ClaudeCode' + c.reset);
18
18
  console.log(c.cyan + ' ║ ' + c.green + 'weds' + c.white + ' 配置或安装 weds ClaudeCode' + c.reset);
19
+ console.log(c.cyan + ' ║ ' + c.green + 'ucx' + c.white + ' 切换 Codex 账号' + c.reset);
20
+ console.log(c.cyan + ' ║ ' + c.green + 'jgrok' + c.white + ' 带代理环境启动 grok' + c.reset);
19
21
  console.log(c.cyan + ' ╚══════════════════════════════════════════╝' + c.reset);
20
22
  console.log('');
package/bin/ucx.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ var os = require('os');
6
+ var path = require('path');
7
+ var { Command } = require('commander');
8
+ var packageJson = require('../package.json');
9
+
10
+ /**
11
+ * 构建 ucx 运行配置,只解析主目录,禁止创建空的 ~/.codex
12
+ * @returns {{ homeDir: string }}
13
+ */
14
+ function getBuildCfg() {
15
+ var homeDir = process.env.JCC_TEST_HOME || os.homedir();
16
+ return { homeDir: homeDir };
17
+ }
18
+
19
+ /**
20
+ * 动态加载 lib 下的命令模块,便于本地开发时读取最新文件
21
+ * @param {string} libName lib 目录下的文件名(不含 .js)
22
+ * @returns {Function}
23
+ */
24
+ function loadLib(libName) {
25
+ var libPath = path.resolve(__dirname, '../lib/' + libName + '.js');
26
+ delete require.cache[require.resolve(libPath)];
27
+ return require(libPath);
28
+ }
29
+
30
+ /**
31
+ * 拉取远程配置(含自动升级)后执行账号切换
32
+ * @returns {Promise<void>}
33
+ */
34
+ async function runSwitch() {
35
+ var buildCfg = getBuildCfg();
36
+ var utils = loadLib('ai_utils');
37
+ await utils.fetchConfig(buildCfg);
38
+ await utils.showNotice();
39
+ await loadLib('ucx')({}, buildCfg);
40
+ }
41
+
42
+ /**
43
+ * 运行 ucx CLI,可供命令行直接执行或被 base_parts_ai require 调用
44
+ * @param {Array<string>} args 用户参数,不包含 node 和脚本路径
45
+ * @returns {Promise<void>}
46
+ */
47
+ async function runCli(args) {
48
+ var cliArgs = (args || []).slice();
49
+ var program = new Command();
50
+
51
+ program
52
+ .name('ucx')
53
+ .description('通过目录改名切换 Codex 账号')
54
+ .version(packageJson.version)
55
+ .action(async function () {
56
+ await runSwitch();
57
+ });
58
+
59
+ // 无参数时直接进入切换,避免 commander 空参数不触发根 action
60
+ if (cliArgs.length <= 0) {
61
+ await runSwitch();
62
+ return;
63
+ }
64
+
65
+ // 使用 from:user 避免读取或修改全局 process.argv,便于被主入口 require 调用
66
+ await program.parseAsync(cliArgs, { from: 'user' });
67
+ }
68
+
69
+ if (require.main === module) {
70
+ runCli(process.argv.slice(2)).catch(function (err) {
71
+ console.error('ucx 执行失败:', err.message);
72
+ process.exit(1);
73
+ });
74
+ }
75
+
76
+ module.exports = runCli;
package/lib/ai_utils.js CHANGED
@@ -51,13 +51,15 @@ async function fetchRemoteText(url, timeoutMs) {
51
51
 
52
52
  /**
53
53
  * 从当前入口文件名推断正在运行的 CLI 命令名
54
- * @returns {string} jcc、jcx、weds 或空字符串
54
+ * @returns {string} jcc、jcx、weds、ucx、jgrok 或空字符串
55
55
  */
56
56
  function getCurrentCliName() {
57
57
  var entryName = path.basename(process.argv[1] || '').toLowerCase();
58
58
  if (entryName === 'jcx' || entryName === 'jcx.js') return 'jcx';
59
59
  if (entryName === 'jcc' || entryName === 'jcc.js') return 'jcc';
60
60
  if (entryName === 'weds' || entryName === 'weds.js') return 'weds';
61
+ if (entryName === 'ucx' || entryName === 'ucx.js') return 'ucx';
62
+ if (entryName === 'jgrok' || entryName === 'jgrok.js') return 'jgrok';
61
63
  return '';
62
64
  }
63
65
 
package/lib/jgrok.js ADDED
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * jgrok 命令:检测并安装 grok,注入 ~/.grok/.env 后启动官方 grok
5
+ * 首次创建 .env 只提示编辑,不启动 grok
6
+ */
7
+
8
+ var fs = require('fs');
9
+ var os = require('os');
10
+ var path = require('path');
11
+ var childProcess = require('child_process');
12
+
13
+ /** ~/.grok/.env 自定义代理示例内容 */
14
+ var GROK_ENV_DEMO = [
15
+ 'HTTP_PROXY=http://127.0.0.1:11811',
16
+ 'HTTPS_PROXY=http://127.0.0.1:11811',
17
+ 'ALL_PROXY=http://127.0.0.1:11811',
18
+ 'NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16',
19
+ '',
20
+ ].join('\n');
21
+
22
+ /** 缺失 grok 时走腾讯云 npm 源安装,不加 --force,安装时不注入环境变量 */
23
+ var GROK_INSTALL_CMD = 'npm install -g @xai-official/grok --registry=https://mirrors.cloud.tencent.com/npm/';
24
+
25
+ /**
26
+ * 解析用户主目录:优先 buildCfg.homeDir,再 JCC_TEST_HOME,最后 os.homedir()
27
+ * @param {object} [buildCfg] 入口传入的配置
28
+ * @returns {string}
29
+ */
30
+ function getHomeDir(buildCfg) {
31
+ if (buildCfg && buildCfg.homeDir) {
32
+ return buildCfg.homeDir;
33
+ }
34
+ return process.env.JCC_TEST_HOME || os.homedir();
35
+ }
36
+
37
+ /**
38
+ * 获取 ~/.grok 目录路径
39
+ * @param {string} homeDir 用户主目录
40
+ * @returns {string}
41
+ */
42
+ function getGrokDir(homeDir) {
43
+ return path.join(homeDir, '.grok');
44
+ }
45
+
46
+ /**
47
+ * 获取 ~/.grok/.env 绝对路径
48
+ * @param {string} homeDir 用户主目录
49
+ * @returns {string}
50
+ */
51
+ function getGrokEnvPath(homeDir) {
52
+ return path.join(getGrokDir(homeDir), '.env');
53
+ }
54
+
55
+ /**
56
+ * 解析 KEY=VALUE 文本,跳过空行和 # 注释,去掉值两端成对引号
57
+ * @param {string} content .env 文件内容
58
+ * @returns {object}
59
+ */
60
+ function parseEnvFile(content) {
61
+ var env = {};
62
+ String(content || '').replace(/^\uFEFF/, '').split(/\r?\n/).forEach(function (line) {
63
+ var trimmed = line.trim();
64
+ if (!trimmed || trimmed.charAt(0) === '#') {
65
+ return;
66
+ }
67
+ var eq = trimmed.indexOf('=');
68
+ if (eq <= 0) {
69
+ return;
70
+ }
71
+ var key = trimmed.slice(0, eq).trim();
72
+ var value = trimmed.slice(eq + 1).trim();
73
+ var quote = value.charAt(0);
74
+ if ((quote === '"' || quote === "'") && value.length >= 2 && value.charAt(value.length - 1) === quote) {
75
+ value = value.slice(1, -1);
76
+ }
77
+ env[key] = value;
78
+ });
79
+ return env;
80
+ }
81
+
82
+ /**
83
+ * 检测本机是否存在指定命令:Windows 用 where,其它用 which
84
+ * @param {string} name 命令名
85
+ * @param {object} [options] 可选配置,execImpl 用于注入 execSync
86
+ * @returns {boolean}
87
+ */
88
+ function commandExists(name, options) {
89
+ options = options || {};
90
+ var exec = options.execImpl || childProcess.execSync;
91
+ var cmd = process.platform === 'win32' ? ('where ' + name) : ('which ' + name);
92
+ try {
93
+ exec(cmd, { stdio: 'ignore' });
94
+ return true;
95
+ } catch (e) {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 本机没有 grok 时通过腾讯云 npm 源全局安装,安装失败向上抛错
102
+ * 安装不注入 ~/.grok/.env,直接走镜像源
103
+ * @param {object} [options] 可选配置,execImpl 用于注入 execSync
104
+ * @returns {void}
105
+ */
106
+ function ensureGrokInstalled(options) {
107
+ if (commandExists('grok', options)) {
108
+ return;
109
+ }
110
+ options = options || {};
111
+ var exec = options.execImpl || childProcess.execSync;
112
+ console.log('未检测到 grok,正在从腾讯云 npm 源安装 @xai-official/grok ...');
113
+ exec(GROK_INSTALL_CMD, { stdio: 'inherit' });
114
+ }
115
+
116
+ /**
117
+ * 确保 ~/.grok/.env 存在;不存在则写入代理示例,已存在则不覆盖
118
+ * @param {string} homeDir 用户主目录
119
+ * @returns {{ created: boolean, envPath: string }}
120
+ */
121
+ function ensureGrokEnvFile(homeDir) {
122
+ var grokDir = getGrokDir(homeDir);
123
+ var envPath = getGrokEnvPath(homeDir);
124
+ if (!fs.existsSync(grokDir)) {
125
+ fs.mkdirSync(grokDir, { recursive: true });
126
+ }
127
+ if (fs.existsSync(envPath)) {
128
+ return { created: false, envPath: envPath };
129
+ }
130
+ fs.writeFileSync(envPath, GROK_ENV_DEMO, 'utf8');
131
+ return { created: true, envPath: envPath };
132
+ }
133
+
134
+ /**
135
+ * 将 .env 解析结果覆盖到当前进程环境副本上,供子进程使用
136
+ * @param {string} content .env 文件内容
137
+ * @returns {object}
138
+ */
139
+ function buildChildEnv(content) {
140
+ return Object.assign({}, process.env, parseEnvFile(content));
141
+ }
142
+
143
+ /**
144
+ * 启动官方 grok,Windows 走 shell 以便找到 grok.cmd
145
+ * @param {Array<string>} args 透传给 grok 的参数
146
+ * @param {object} childEnv 注入后的环境变量
147
+ * @param {object} [options] 可选配置,spawnImpl 用于注入 spawnSync
148
+ * @returns {number} grok 退出码
149
+ */
150
+ function spawnGrok(args, childEnv, options) {
151
+ options = options || {};
152
+ var spawn = options.spawnImpl || childProcess.spawnSync;
153
+ var result = spawn('grok', args || [], {
154
+ stdio: 'inherit',
155
+ shell: process.platform === 'win32',
156
+ env: childEnv,
157
+ }) || {};
158
+ if (result.error) {
159
+ throw result.error;
160
+ }
161
+ if (result.signal) {
162
+ return 1;
163
+ }
164
+ return typeof result.status === 'number' ? result.status : 0;
165
+ }
166
+
167
+ /**
168
+ * 执行 jgrok:先处理 .env,首次创建则提示编辑;否则注入环境后安装并启动 grok
169
+ * @param {Array<string>} args 透传给 grok 的参数
170
+ * @param {object} buildCfg 含 homeDir 的配置
171
+ * @param {object} [options] 可选配置,execImpl/spawnImpl 用于单测注入
172
+ * @returns {Promise<number>} 退出码
173
+ */
174
+ async function runJgrok(args, buildCfg, options) {
175
+ options = options || {};
176
+ var homeDir = getHomeDir(buildCfg);
177
+ var envResult = ensureGrokEnvFile(homeDir);
178
+ if (envResult.created) {
179
+ console.log('已首次创建 grok 环境文件,请编辑后再运行 jgrok:');
180
+ console.log(envResult.envPath);
181
+ return 0;
182
+ }
183
+ var childEnv = buildChildEnv(fs.readFileSync(envResult.envPath, 'utf8'));
184
+ ensureGrokInstalled(options);
185
+ return spawnGrok(args || [], childEnv, options);
186
+ }
187
+
188
+ runJgrok._private = {
189
+ GROK_ENV_DEMO: GROK_ENV_DEMO,
190
+ GROK_INSTALL_CMD: GROK_INSTALL_CMD,
191
+ getHomeDir: getHomeDir,
192
+ getGrokDir: getGrokDir,
193
+ getGrokEnvPath: getGrokEnvPath,
194
+ parseEnvFile: parseEnvFile,
195
+ commandExists: commandExists,
196
+ ensureGrokInstalled: ensureGrokInstalled,
197
+ ensureGrokEnvFile: ensureGrokEnvFile,
198
+ buildChildEnv: buildChildEnv,
199
+ spawnGrok: spawnGrok,
200
+ };
201
+
202
+ module.exports = runJgrok;
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 项目目录中 AGENTS.md 与 CLAUDE.md 的互转重命名
5
+ * 扫描范围:当前执行目录及其一层子目录,不递归、不覆盖、不跟随符号链接
6
+ */
7
+
8
+ var fs = require('fs');
9
+ var path = require('path');
10
+ var inquirer = require('inquirer');
11
+
12
+ // 菜单哨兵值,避免与服务端渠道 id 冲突。
13
+ var RENAME_AGENTS_TO_CLAUDE = '__rename_agents_to_claude__';
14
+ var RENAME_CLAUDE_TO_AGENTS = '__rename_claude_to_agents__';
15
+ var AGENTS_MD = 'AGENTS.md';
16
+ var CLAUDE_MD = 'CLAUDE.md';
17
+
18
+ /**
19
+ * 判断渠道列表选中值是否为项目 md 重命名项
20
+ * @param {string} channelId 菜单选中值
21
+ * @returns {boolean}
22
+ */
23
+ function isRenameChoice(channelId) {
24
+ return channelId === RENAME_AGENTS_TO_CLAUDE || channelId === RENAME_CLAUDE_TO_AGENTS;
25
+ }
26
+
27
+ /**
28
+ * 生成重命名确认文案
29
+ * @param {string} fromName 源文件名
30
+ * @param {string} toName 目标文件名
31
+ * @returns {string}
32
+ */
33
+ function confirmRenameMessage(fromName, toName) {
34
+ return '确认将当前目录及一层子目录中的 ' + fromName + ' 重命名为 ' + toName + ' 吗?';
35
+ }
36
+
37
+ /**
38
+ * 按方向返回单条重命名菜单项:jcc 仅 AGENTS→CLAUDE,jcx 仅 CLAUDE→AGENTS
39
+ * @param {string} renameValue 重命名哨兵值
40
+ * @returns {{name: string, value: string}|null}
41
+ */
42
+ function getRenameChoice(renameValue) {
43
+ if (renameValue === RENAME_AGENTS_TO_CLAUDE) {
44
+ return {
45
+ name: 'AGENTS.md 转 CLAUDE.md — 重命名当前目录及一层子目录中的文件',
46
+ value: RENAME_AGENTS_TO_CLAUDE,
47
+ };
48
+ }
49
+ if (renameValue === RENAME_CLAUDE_TO_AGENTS) {
50
+ return {
51
+ name: 'CLAUDE.md 转 AGENTS.md — 重命名当前目录及一层子目录中的文件',
52
+ value: RENAME_CLAUDE_TO_AGENTS,
53
+ };
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * 在清除项之前追加对应方向的一条重命名入口
60
+ * @param {Array<object>} choices inquirer 选项数组
61
+ * @param {string} renameValue 重命名哨兵值
62
+ * @returns {Array<object>}
63
+ */
64
+ function appendRenameChoice(choices, renameValue) {
65
+ var item = getRenameChoice(renameValue);
66
+ if (item) {
67
+ choices.push(item);
68
+ }
69
+ return choices;
70
+ }
71
+
72
+ /**
73
+ * 列出需要扫描的目录:root 本身 + 一层非隐藏、非符号链接子目录
74
+ * @param {string} rootDir 项目根目录
75
+ * @returns {string[]}
76
+ */
77
+ function listScanDirs(rootDir) {
78
+ var root = path.resolve(rootDir);
79
+ var dirs = [root];
80
+ var entries;
81
+ try {
82
+ entries = fs.readdirSync(root, { withFileTypes: true });
83
+ } catch (e) {
84
+ console.warn('⚠️ 读取目录失败(' + root + '): ' + e.message);
85
+ return dirs;
86
+ }
87
+ entries.forEach(function (dirent) {
88
+ // 跳过点开头目录,避免改到 .git / .claude / .codex
89
+ if (dirent.name.charAt(0) === '.') {
90
+ return;
91
+ }
92
+ // 不跟随符号链接,只用 Dirent 自身类型判断
93
+ if (typeof dirent.isSymbolicLink === 'function' && dirent.isSymbolicLink()) {
94
+ return;
95
+ }
96
+ if (!dirent.isDirectory()) {
97
+ return;
98
+ }
99
+ dirs.push(path.join(root, dirent.name));
100
+ });
101
+ return dirs;
102
+ }
103
+
104
+ /**
105
+ * 将 root 及一层子目录中的 fromName 重命名为 toName,目标已存在则跳过
106
+ * @param {string} rootDir 项目根目录
107
+ * @param {string} fromName 源文件名
108
+ * @param {string} toName 目标文件名
109
+ * @returns {{renamed: number, skippedExisting: number, skippedMissing: number, errors: number}}
110
+ */
111
+ function renameProjectMdFiles(rootDir, fromName, toName) {
112
+ var root = path.resolve(rootDir);
113
+ var dirs = listScanDirs(root);
114
+ var result = {
115
+ renamed: 0,
116
+ skippedExisting: 0,
117
+ skippedMissing: 0,
118
+ errors: 0,
119
+ };
120
+
121
+ dirs.forEach(function (dir) {
122
+ var fromPath = path.join(dir, fromName);
123
+ var toPath = path.join(dir, toName);
124
+ try {
125
+ if (!fs.existsSync(fromPath) || !fs.statSync(fromPath).isFile()) {
126
+ result.skippedMissing += 1;
127
+ return;
128
+ }
129
+ // 目标已存在则不覆盖,Windows 大小写不敏感时 claude.md 也会命中
130
+ if (fs.existsSync(toPath)) {
131
+ console.warn('⚠️ 跳过(目标已存在): ' + toPath);
132
+ result.skippedExisting += 1;
133
+ return;
134
+ }
135
+ fs.renameSync(fromPath, toPath);
136
+ var relative = path.relative(root, dir);
137
+ var displayDir = relative || '.';
138
+ console.log(fromName + ' -> ' + toName + ' (' + displayDir + ')');
139
+ result.renamed += 1;
140
+ } catch (e) {
141
+ console.warn('⚠️ 重命名失败(' + fromPath + '): ' + e.message);
142
+ result.errors += 1;
143
+ }
144
+ });
145
+
146
+ if (result.renamed === 0) {
147
+ if (result.skippedExisting > 0) {
148
+ console.log('未执行重命名:目标文件已存在。');
149
+ } else {
150
+ console.log('未找到可重命名的 ' + fromName + '。');
151
+ }
152
+ } else {
153
+ console.log('✅ 已重命名 ' + result.renamed + ' 个文件: ' + fromName + ' -> ' + toName);
154
+ }
155
+ return result;
156
+ }
157
+
158
+ /**
159
+ * 处理项目 md 重命名菜单项:确认后按 cwd 及一层子目录改名
160
+ * @param {string} channelId 菜单选中值
161
+ * @returns {Promise<boolean>} 是否已作为重命名项处理(含用户取消)
162
+ */
163
+ async function handleRenameChoice(channelId) {
164
+ if (!isRenameChoice(channelId)) {
165
+ return false;
166
+ }
167
+ var fromName = channelId === RENAME_AGENTS_TO_CLAUDE ? AGENTS_MD : CLAUDE_MD;
168
+ var toName = channelId === RENAME_AGENTS_TO_CLAUDE ? CLAUDE_MD : AGENTS_MD;
169
+ var answer = await inquirer.prompt([{
170
+ type: 'confirm',
171
+ name: 'confirmed',
172
+ message: confirmRenameMessage(fromName, toName),
173
+ default: false,
174
+ }]);
175
+ if (!answer.confirmed) {
176
+ console.log('已取消重命名。');
177
+ return true;
178
+ }
179
+ renameProjectMdFiles(process.cwd(), fromName, toName);
180
+ return true;
181
+ }
182
+
183
+ module.exports = {
184
+ RENAME_AGENTS_TO_CLAUDE: RENAME_AGENTS_TO_CLAUDE,
185
+ RENAME_CLAUDE_TO_AGENTS: RENAME_CLAUDE_TO_AGENTS,
186
+ AGENTS_MD: AGENTS_MD,
187
+ CLAUDE_MD: CLAUDE_MD,
188
+ isRenameChoice: isRenameChoice,
189
+ confirmRenameMessage: confirmRenameMessage,
190
+ getRenameChoice: getRenameChoice,
191
+ appendRenameChoice: appendRenameChoice,
192
+ listScanDirs: listScanDirs,
193
+ renameProjectMdFiles: renameProjectMdFiles,
194
+ handleRenameChoice: handleRenameChoice,
195
+ };
package/lib/setapi_cc.js CHANGED
@@ -12,6 +12,7 @@ var inquirer = require('inquirer');
12
12
  var fs = require('fs');
13
13
  var utils = require('./ai_utils');
14
14
  var childProcess = require('child_process');
15
+ var projectMdRename = require('./project_md_rename');
15
16
 
16
17
  // Claude 配置清除选项使用固定内部值,避免与服务端渠道 id 冲突。
17
18
  var CLEAR_CLAUDE_CONFIG = '__clear_claude_config__';
@@ -84,7 +85,8 @@ async function setapiCc(cmd, buildCfg) {
84
85
  value: c.id,
85
86
  };
86
87
  });
87
- // 清除入口固定放在渠道列表最后,即使服务端渠道为空也可恢复官方配置。
88
+ // jcc/weds 只提供 AGENTS→CLAUDE;清除入口固定最后,即使服务端渠道为空也可恢复官方配置。
89
+ projectMdRename.appendRenameChoice(choices, projectMdRename.RENAME_AGENTS_TO_CLAUDE);
88
90
  choices.push({
89
91
  name: '>>>>清除Claude配置<<<< — 删除并恢复官方默认配置',
90
92
  value: CLEAR_CLAUDE_CONFIG,
@@ -100,6 +102,11 @@ async function setapiCc(cmd, buildCfg) {
100
102
  }]);
101
103
  var channelId = answers.channelId;
102
104
 
105
+ // 项目 md 重命名不写渠道配置,必须在空渠道校验之前处理。
106
+ if (await projectMdRename.handleRenameChoice(channelId)) {
107
+ return;
108
+ }
109
+
103
110
  if (channelId === CLEAR_CLAUDE_CONFIG) {
104
111
  var clearAnswer = await inquirer.prompt([{
105
112
  type: 'confirm',
@@ -276,6 +283,8 @@ setapiCc._private = {
276
283
  validateRequiredInput: validateRequiredInput,
277
284
  clearClaudeConfig: clearClaudeConfig,
278
285
  CLEAR_CLAUDE_CONFIG: CLEAR_CLAUDE_CONFIG,
286
+ RENAME_AGENTS_TO_CLAUDE: projectMdRename.RENAME_AGENTS_TO_CLAUDE,
287
+ RENAME_CLAUDE_TO_AGENTS: projectMdRename.RENAME_CLAUDE_TO_AGENTS,
279
288
  };
280
289
 
281
290
  module.exports = setapiCc;
package/lib/setapi_cx.js CHANGED
@@ -11,6 +11,7 @@ var inquirer = require('inquirer');
11
11
  var utils = require('./ai_utils');
12
12
  var childProcess = require('child_process');
13
13
  var TOML = require('@ltd/j-toml');
14
+ var projectMdRename = require('./project_md_rename');
14
15
 
15
16
  // Codex 配置清除选项使用固定内部值,避免与服务端渠道 id 冲突。
16
17
  var CLEAR_CODEX_CONFIG = '__clear_codex_config__';
@@ -444,7 +445,8 @@ async function setapiCx(cmd, buildCfg) {
444
445
  value: c.id,
445
446
  };
446
447
  });
447
- // 清除入口固定放在渠道列表最后,即使服务端渠道为空也可恢复官方配置。
448
+ // jcx 只提供 CLAUDE→AGENTS;清除入口固定最后,即使服务端渠道为空也可恢复官方配置。
449
+ projectMdRename.appendRenameChoice(choices, projectMdRename.RENAME_CLAUDE_TO_AGENTS);
448
450
  choices.push({
449
451
  name: '>>>>清除Codex配置<<<< — 删除并恢复官方默认配置',
450
452
  value: CLEAR_CODEX_CONFIG,
@@ -457,6 +459,11 @@ async function setapiCx(cmd, buildCfg) {
457
459
  choices: choices,
458
460
  default: getCurrentCodexChannelId(auth, toml) || undefined,
459
461
  }]);
462
+ // 项目 md 重命名不写渠道配置,必须在空渠道校验之前处理。
463
+ if (await projectMdRename.handleRenameChoice(answers.channelId)) {
464
+ return;
465
+ }
466
+
460
467
  if (answers.channelId === CLEAR_CODEX_CONFIG) {
461
468
  var clearAnswer = await inquirer.prompt([{
462
469
  type: 'confirm',
@@ -555,6 +562,8 @@ setapiCx._private = {
555
562
  validateRequiredInput: validateRequiredInput,
556
563
  clearCodexConfig: clearCodexConfig,
557
564
  CLEAR_CODEX_CONFIG: CLEAR_CODEX_CONFIG,
565
+ RENAME_AGENTS_TO_CLAUDE: projectMdRename.RENAME_AGENTS_TO_CLAUDE,
566
+ RENAME_CLAUDE_TO_AGENTS: projectMdRename.RENAME_CLAUDE_TO_AGENTS,
558
567
  };
559
568
 
560
569
  module.exports = setapiCx;
package/lib/ucx.js ADDED
@@ -0,0 +1,562 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ucx 命令:通过重命名 ~/.codex 与 ~/.codex_<账号名> 切换 Codex 账号
5
+ * 账号名优先用非空 OPENAI_API_KEY 的脱敏短名,否则用 JWT email
6
+ * 不创建空的 ~/.codex,避免把空目录当成当前账号
7
+ */
8
+
9
+ var fs = require('fs');
10
+ var os = require('os');
11
+ var path = require('path');
12
+ var childProcess = require('child_process');
13
+ var inquirer = require('inquirer');
14
+ var utils = require('./ai_utils');
15
+
16
+ /** 无法解析账号名时的展示名 */
17
+ var UNKNOWN_EMAIL_LABEL = '未知邮箱';
18
+
19
+ /** 列表末尾固定项:移除当前账号,以便登录下一个账号 */
20
+ var REMOVE_CURRENT_ACCOUNT = '__remove_current_codex__';
21
+
22
+ /** ~/.codex/.env 自定义代理示例内容 */
23
+ var CODEX_ENV_DEMO = [
24
+ 'HTTP_PROXY=http://127.0.0.1:11811',
25
+ 'HTTPS_PROXY=http://127.0.0.1:11811',
26
+ 'ALL_PROXY=http://127.0.0.1:11811',
27
+ 'NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16',
28
+ '',
29
+ ].join('\n');
30
+
31
+ /**
32
+ * 解析用户主目录:优先 buildCfg.homeDir,再 JCC_TEST_HOME,最后 os.homedir()
33
+ * @param {object} [buildCfg] 入口传入的配置
34
+ * @returns {string}
35
+ */
36
+ function getHomeDir(buildCfg) {
37
+ if (buildCfg && buildCfg.homeDir) {
38
+ return buildCfg.homeDir;
39
+ }
40
+ return process.env.JCC_TEST_HOME || os.homedir();
41
+ }
42
+
43
+ /**
44
+ * 判断目录名是否为 Codex 账号目录(仅 .codex 或 .codex_*)
45
+ * @param {string} name 目录名
46
+ * @returns {boolean}
47
+ */
48
+ function isCodexAccountDirName(name) {
49
+ return name === '.codex' || name.indexOf('.codex_') === 0;
50
+ }
51
+
52
+ /**
53
+ * 将账号名中 Windows 非法文件名字符替换为下划线,保留 @.+-
54
+ * @param {string} accountName 邮箱或脱敏 Key 短名
55
+ * @returns {string}
56
+ */
57
+ function sanitizeEmailForDir(accountName) {
58
+ return String(accountName || '').replace(/[<>:"/\\|?*]/g, '_');
59
+ }
60
+
61
+ /**
62
+ * 解码 JWT 第二段 payload,不校验签名
63
+ * @param {string} token JWT 字符串
64
+ * @returns {object|null}
65
+ */
66
+ function decodeJwtPayload(token) {
67
+ if (typeof token !== 'string' || !token) {
68
+ return null;
69
+ }
70
+ var parts = token.split('.');
71
+ if (parts.length < 2 || !parts[1]) {
72
+ return null;
73
+ }
74
+ try {
75
+ var json = Buffer.from(parts[1], 'base64url').toString('utf8');
76
+ return JSON.parse(json);
77
+ } catch (e) {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 从 payload 中取出非空 email
84
+ * @param {object|null} payload JWT payload
85
+ * @returns {string}
86
+ */
87
+ function emailFromPayload(payload) {
88
+ if (!payload || typeof payload.email !== 'string') {
89
+ return '';
90
+ }
91
+ return payload.email;
92
+ }
93
+
94
+ /**
95
+ * 从 tokens 字段提取 email:兼容 JWT 字符串、id_token、access_token
96
+ * @param {*} tokens auth.json 的 tokens 字段
97
+ * @returns {string}
98
+ */
99
+ function extractEmailFromTokens(tokens) {
100
+ if (typeof tokens === 'string') {
101
+ return emailFromPayload(decodeJwtPayload(tokens));
102
+ }
103
+ if (!tokens || typeof tokens !== 'object') {
104
+ return '';
105
+ }
106
+ var fromId = emailFromPayload(decodeJwtPayload(tokens.id_token));
107
+ if (fromId) {
108
+ return fromId;
109
+ }
110
+ return emailFromPayload(decodeJwtPayload(tokens.access_token));
111
+ }
112
+
113
+ /**
114
+ * 读取 auth.json 中的非空 OPENAI_API_KEY
115
+ * @param {object|null} auth auth.json 对象
116
+ * @returns {string}
117
+ */
118
+ function getOpenAiApiKey(auth) {
119
+ if (!auth || typeof auth.OPENAI_API_KEY !== 'string') {
120
+ return '';
121
+ }
122
+ return auth.OPENAI_API_KEY.trim();
123
+ }
124
+
125
+ /**
126
+ * 读取账号目录的展示/停放名:非空 OPENAI_API_KEY 优先用脱敏短名,否则用 JWT email
127
+ * @param {string} authJsonPath auth.json 路径
128
+ * @returns {string}
129
+ */
130
+ function readAccountName(authJsonPath) {
131
+ var auth = utils.readJsonFile(authJsonPath, null);
132
+ if (!auth) {
133
+ return '';
134
+ }
135
+ var apiKey = getOpenAiApiKey(auth);
136
+ if (apiKey) {
137
+ return utils.maskKey(apiKey);
138
+ }
139
+ return extractEmailFromTokens(auth.tokens);
140
+ }
141
+
142
+ /**
143
+ * 读取账号目录 auth.json 中的账号名(兼容旧测试名)
144
+ * @param {string} authJsonPath auth.json 路径
145
+ * @returns {string}
146
+ */
147
+ function readAccountEmail(authJsonPath) {
148
+ return readAccountName(authJsonPath);
149
+ }
150
+
151
+ /**
152
+ * 扫描用户主目录一层的 .codex 与 .codex_* 目录,不递归
153
+ * @param {string} homeDir 用户主目录
154
+ * @returns {Array<string>} 目录名列表
155
+ */
156
+ function listCodexAccountDirs(homeDir) {
157
+ var entries;
158
+ try {
159
+ entries = fs.readdirSync(homeDir, { withFileTypes: true });
160
+ } catch (e) {
161
+ return [];
162
+ }
163
+ var names = [];
164
+ entries.forEach(function (ent) {
165
+ if (!ent.isDirectory()) {
166
+ return;
167
+ }
168
+ if (isCodexAccountDirName(ent.name)) {
169
+ names.push(ent.name);
170
+ }
171
+ });
172
+ return names;
173
+ }
174
+
175
+ /**
176
+ * 把账号目录转成统一记录
177
+ * @param {string} homeDir 用户主目录
178
+ * @param {string} dirName 目录名
179
+ * @returns {object}
180
+ */
181
+ function buildAccountRecord(homeDir, dirName) {
182
+ var dirPath = path.join(homeDir, dirName);
183
+ var accountName = readAccountName(path.join(dirPath, 'auth.json'));
184
+ var parkedName = accountName ? ('.codex_' + sanitizeEmailForDir(accountName)) : '';
185
+ return {
186
+ dirName: dirName,
187
+ dirPath: dirPath,
188
+ accountName: accountName,
189
+ email: accountName,
190
+ displayEmail: accountName || UNKNOWN_EMAIL_LABEL,
191
+ parkedName: parkedName,
192
+ isCurrent: dirName === '.codex',
193
+ };
194
+ }
195
+
196
+ /**
197
+ * 读取全部 Codex 账号记录,当前账号置顶
198
+ * @param {string} homeDir 用户主目录
199
+ * @returns {Array<object>}
200
+ */
201
+ function buildAccountRecords(homeDir) {
202
+ var records = listCodexAccountDirs(homeDir).map(function (dirName) {
203
+ return buildAccountRecord(homeDir, dirName);
204
+ });
205
+ records.sort(function (a, b) {
206
+ if (a.isCurrent !== b.isCurrent) {
207
+ return a.isCurrent ? -1 : 1;
208
+ }
209
+ var emailCmp = String(a.displayEmail).localeCompare(String(b.displayEmail));
210
+ if (emailCmp !== 0) {
211
+ return emailCmp;
212
+ }
213
+ return String(a.dirName).localeCompare(String(b.dirName));
214
+ });
215
+ return records;
216
+ }
217
+
218
+ /**
219
+ * 生成 inquirer 账号列表项,value 使用 dirPath 保证唯一
220
+ * @param {Array<object>} records 账号记录
221
+ * @returns {Array<{name: string, value: string}>}
222
+ */
223
+ function buildAccountChoices(records) {
224
+ return records.map(function (record) {
225
+ var suffix = ' (~/' + record.dirName + ')';
226
+ var name = record.displayEmail + suffix;
227
+ if (record.isCurrent) {
228
+ name += ' 当前';
229
+ }
230
+ return { name: name, value: record.dirPath };
231
+ });
232
+ }
233
+
234
+ /**
235
+ * 生成完整选项:账号列表末尾固定追加「移除当前账号」,无账号时也要显示
236
+ * @param {Array<object>} records 账号记录
237
+ * @returns {Array<{name: string, value: string}>}
238
+ */
239
+ function buildUcxChoices(records) {
240
+ var choices = buildAccountChoices(records);
241
+ choices.push({
242
+ name: '*移除当前账号*',
243
+ value: REMOVE_CURRENT_ACCOUNT,
244
+ });
245
+ return choices;
246
+ }
247
+
248
+ /**
249
+ * 写入 ~/.codex/.env 代理示例;目录不存在时先创建
250
+ * @param {string} homeDir 用户主目录
251
+ * @returns {string} 写入的 .env 路径
252
+ */
253
+ function writeCodexEnvDemo(homeDir) {
254
+ var codexDir = path.join(homeDir, '.codex');
255
+ if (!fs.existsSync(codexDir)) {
256
+ fs.mkdirSync(codexDir, { recursive: true });
257
+ }
258
+ var envPath = path.join(codexDir, '.env');
259
+ fs.writeFileSync(envPath, CODEX_ENV_DEMO, 'utf8');
260
+ return envPath;
261
+ }
262
+
263
+ /**
264
+ * 询问是否新建 ~/.codex/.env 用于自定义代理
265
+ * @param {string} homeDir 用户主目录
266
+ * @returns {Promise<boolean>} 是否已写入
267
+ */
268
+ async function maybeCreateCodexEnv(homeDir) {
269
+ var envAnswer = await inquirer.prompt([{
270
+ type: 'confirm',
271
+ name: 'confirmed',
272
+ message: '是否新建 ~/.codex/.env 用于自定义代理?',
273
+ default: false,
274
+ }]);
275
+ if (!envAnswer.confirmed) {
276
+ return false;
277
+ }
278
+ var envPath = writeCodexEnvDemo(homeDir);
279
+ console.log('已写入 ' + envPath);
280
+ return true;
281
+ }
282
+
283
+ /**
284
+ * 移除当前账号:有登录信息则停放改名;三无目录与切换时相同,询问删除或手工命名
285
+ * @param {string} homeDir 用户主目录
286
+ * @param {object|null} currentRecord 当前 ~/.codex 记录
287
+ * @param {object} [options] 可选配置,execImpl 用于注入杀进程实现
288
+ * @returns {Promise<{ cancelled: boolean }>}
289
+ */
290
+ async function removeCurrentAccount(homeDir, currentRecord, options) {
291
+ options = options || {};
292
+ var confirmAnswer = await inquirer.prompt([{
293
+ type: 'confirm',
294
+ name: 'confirmed',
295
+ message: '确认移除当前账号,以便登录新账号吗?',
296
+ default: false,
297
+ }]);
298
+ if (!confirmAnswer.confirmed) {
299
+ console.log('已取消移除当前账号。');
300
+ return { cancelled: true };
301
+ }
302
+
303
+ var currentPath = path.join(homeDir, '.codex');
304
+ var unnamedAction = null;
305
+ if (fs.existsSync(currentPath) && !(currentRecord && currentRecord.accountName)) {
306
+ unnamedAction = await promptUnnamedCurrentAction();
307
+ }
308
+
309
+ killCodexProcesses(options.execImpl);
310
+ if (!fs.existsSync(currentPath)) {
311
+ console.log('当前没有 ~/.codex 目录。');
312
+ } else if (unnamedAction) {
313
+ applyUnnamedCurrentAction(homeDir, currentPath, unnamedAction);
314
+ } else {
315
+ var parkedName = allocateParkedDirName(homeDir, currentRecord.accountName);
316
+ var parkedPath = path.join(homeDir, parkedName);
317
+ fs.renameSync(currentPath, parkedPath);
318
+ console.log('已移除当前账号,已停放到 ' + parkedPath);
319
+ }
320
+
321
+ await maybeCreateCodexEnv(homeDir);
322
+ return { cancelled: false };
323
+ }
324
+
325
+ /**
326
+ * 终止本机 Codex 进程;测试环境或注入 exec 时避免误杀宿主机进程
327
+ * @param {Function} [execImpl] 可注入的 execSync,便于单测
328
+ * @returns {void}
329
+ */
330
+ function killCodexProcesses(execImpl) {
331
+ if (process.env.JCC_TEST_HOME) {
332
+ return;
333
+ }
334
+ var exec = execImpl || childProcess.execSync;
335
+ var commands = process.platform === 'win32'
336
+ ? ['taskkill /F /IM codex.exe', 'taskkill /F /IM codex']
337
+ : ['pkill -x codex'];
338
+ commands.forEach(function (cmd) {
339
+ try {
340
+ exec(cmd, { stdio: 'ignore' });
341
+ } catch (e) {
342
+ // 无匹配进程时 taskkill/pkill 常返回非 0,忽略
343
+ }
344
+ });
345
+ }
346
+
347
+ /**
348
+ * 为当前账号分配不冲突的停放目录名;同名已存在时追加 _2、_3,不覆盖
349
+ * @param {string} homeDir 用户主目录
350
+ * @param {string} accountName 邮箱或脱敏 Key 短名
351
+ * @returns {string} 可用的目录名,例如 .codex_a 或 .codex_a_2
352
+ */
353
+ function allocateParkedDirName(homeDir, accountName) {
354
+ var base = '.codex_' + sanitizeEmailForDir(accountName);
355
+ var name = base;
356
+ var index = 2;
357
+ while (fs.existsSync(path.join(homeDir, name))) {
358
+ name = base + '_' + index;
359
+ index += 1;
360
+ }
361
+ return name;
362
+ }
363
+
364
+ /**
365
+ * 校验用户输入的停放目录名,只用于改名,不写回 auth.json
366
+ * @param {string} value 用户输入
367
+ * @returns {boolean|string}
368
+ */
369
+ function validateParkName(value) {
370
+ var trimmed = String(value || '').trim();
371
+ if (!trimmed) {
372
+ return '名称不能为空';
373
+ }
374
+ if (!sanitizeEmailForDir(trimmed)) {
375
+ return '名称包含无效字符';
376
+ }
377
+ return true;
378
+ }
379
+
380
+ /**
381
+ * 三无当前目录:询问是否删除,否则让用户输入停放名(不写回 auth.json)
382
+ * @returns {Promise<{ deleteCurrent: boolean, parkName?: string }>}
383
+ */
384
+ async function promptUnnamedCurrentAction() {
385
+ var deleteAnswer = await inquirer.prompt([{
386
+ type: 'confirm',
387
+ name: 'confirmed',
388
+ message: '当前目录没有登陆账号,是否删除?',
389
+ default: false,
390
+ }]);
391
+ if (deleteAnswer.confirmed) {
392
+ return { deleteCurrent: true };
393
+ }
394
+ var nameAnswer = await inquirer.prompt([{
395
+ type: 'input',
396
+ name: 'accountName',
397
+ message: '请输入用于停放当前目录的名称(不会写回 auth.json)',
398
+ validate: validateParkName,
399
+ }]);
400
+ return { deleteCurrent: false, parkName: String(nameAnswer.accountName || '').trim() };
401
+ }
402
+
403
+ /**
404
+ * 按三无目录的用户选择删除或手工命名停放,名称不写回 auth.json
405
+ * @param {string} homeDir 用户主目录
406
+ * @param {string} currentPath 当前 ~/.codex 路径
407
+ * @param {{ deleteCurrent: boolean, parkName?: string }} action 用户选择
408
+ * @returns {string|null} 停放后的路径,删除时为 null
409
+ */
410
+ function applyUnnamedCurrentAction(homeDir, currentPath, action) {
411
+ if (action.deleteCurrent) {
412
+ fs.rmSync(currentPath, { recursive: true, force: true });
413
+ console.log('已删除空的 ~/.codex 目录。');
414
+ return null;
415
+ }
416
+ var parkedName = allocateParkedDirName(homeDir, action.parkName);
417
+ var parkedPath = path.join(homeDir, parkedName);
418
+ fs.renameSync(currentPath, parkedPath);
419
+ console.log('已将当前目录停放到 ' + parkedPath);
420
+ return parkedPath;
421
+ }
422
+
423
+ /**
424
+ * 校验当前 ~/.codex 是否具备可停放的账号名
425
+ * @param {object|null} currentRecord 当前账号记录,没有当前目录时为 null
426
+ * @returns {void}
427
+ */
428
+ function assertCanParkCurrent(homeDir, currentRecord) {
429
+ if (!currentRecord) {
430
+ return;
431
+ }
432
+ if (!currentRecord.accountName) {
433
+ throw new Error('当前 .codex 无法解析账号名,拒绝停放以免丢失账号目录');
434
+ }
435
+ }
436
+
437
+ /**
438
+ * 执行账号目录切换:停放当前 .codex,再把选中目录改名为 .codex
439
+ * 当前为三无目录时先询问删除或手工命名,名称不写回 auth.json
440
+ * @param {string} homeDir 用户主目录
441
+ * @param {object} selected 选中的账号记录
442
+ * @param {Array<object>} records 全部账号记录
443
+ * @param {object} [options] 可选配置,execImpl 用于注入杀进程实现
444
+ * @returns {Promise<{ skipped: boolean, email?: string }>}
445
+ */
446
+ async function switchCodexAccount(homeDir, selected, records, options) {
447
+ options = options || {};
448
+ if (!selected) {
449
+ throw new Error('未找到要切换的 Codex 账号');
450
+ }
451
+ if (selected.isCurrent) {
452
+ console.log('当前已是 ' + selected.displayEmail + ',无需切换');
453
+ return { skipped: true };
454
+ }
455
+
456
+ var currentRecord = null;
457
+ records.forEach(function (record) {
458
+ if (record.isCurrent) {
459
+ currentRecord = record;
460
+ }
461
+ });
462
+
463
+ var unnamedAction = null;
464
+ if (currentRecord && !currentRecord.accountName) {
465
+ unnamedAction = await promptUnnamedCurrentAction();
466
+ } else {
467
+ assertCanParkCurrent(homeDir, currentRecord);
468
+ }
469
+
470
+ killCodexProcesses(options.execImpl);
471
+
472
+ var parkedPath = null;
473
+ if (currentRecord && unnamedAction) {
474
+ parkedPath = applyUnnamedCurrentAction(homeDir, currentRecord.dirPath, unnamedAction);
475
+ } else if (currentRecord) {
476
+ var parkedName = allocateParkedDirName(homeDir, currentRecord.accountName);
477
+ parkedPath = path.join(homeDir, parkedName);
478
+ fs.renameSync(currentRecord.dirPath, parkedPath);
479
+ }
480
+
481
+ var targetPath = path.join(homeDir, '.codex');
482
+ try {
483
+ fs.renameSync(selected.dirPath, targetPath);
484
+ } catch (e) {
485
+ if (parkedPath) {
486
+ console.error('已将原账号停放到 ' + parkedPath + ',但启用新账号失败,请手动恢复');
487
+ }
488
+ throw e;
489
+ }
490
+
491
+ console.log('已切换到 ' + selected.displayEmail + ',请重新启动 Codex');
492
+ return { skipped: false, email: selected.displayEmail };
493
+ }
494
+
495
+ /**
496
+ * 按路径从记录中查找账号
497
+ * @param {Array<object>} records 账号记录
498
+ * @param {string} dirPath 目录绝对路径
499
+ * @returns {object|undefined}
500
+ */
501
+ function findRecordByDirPath(records, dirPath) {
502
+ var matched;
503
+ records.forEach(function (record) {
504
+ if (record.dirPath === dirPath) {
505
+ matched = record;
506
+ }
507
+ });
508
+ return matched;
509
+ }
510
+
511
+ /**
512
+ * 执行 ucx 交互切换
513
+ * @param {object} cmd commander 选项(当前无业务字段)
514
+ * @param {object} buildCfg 含 homeDir 的配置
515
+ * @returns {Promise<void>}
516
+ */
517
+ async function runUcx(cmd, buildCfg) {
518
+ var homeDir = getHomeDir(buildCfg);
519
+ var records = buildAccountRecords(homeDir);
520
+ var current = findRecordByDirPath(records, path.join(homeDir, '.codex'));
521
+ var answer = await inquirer.prompt([{
522
+ type: 'list',
523
+ name: 'dirPath',
524
+ message: '请选择要切换的 Codex 账号',
525
+ choices: buildUcxChoices(records),
526
+ default: current ? current.dirPath : undefined,
527
+ }]);
528
+
529
+ if (answer.dirPath === REMOVE_CURRENT_ACCOUNT) {
530
+ await removeCurrentAccount(homeDir, current || null, {});
531
+ return;
532
+ }
533
+
534
+ var selected = findRecordByDirPath(records, answer.dirPath);
535
+ await switchCodexAccount(homeDir, selected, records, {});
536
+ }
537
+
538
+ runUcx._private = {
539
+ UNKNOWN_EMAIL_LABEL: UNKNOWN_EMAIL_LABEL,
540
+ REMOVE_CURRENT_ACCOUNT: REMOVE_CURRENT_ACCOUNT,
541
+ CODEX_ENV_DEMO: CODEX_ENV_DEMO,
542
+ getHomeDir: getHomeDir,
543
+ sanitizeEmailForDir: sanitizeEmailForDir,
544
+ decodeJwtPayload: decodeJwtPayload,
545
+ extractEmailFromTokens: extractEmailFromTokens,
546
+ getOpenAiApiKey: getOpenAiApiKey,
547
+ readAccountName: readAccountName,
548
+ readAccountEmail: readAccountEmail,
549
+ listCodexAccountDirs: listCodexAccountDirs,
550
+ buildAccountRecords: buildAccountRecords,
551
+ buildAccountChoices: buildAccountChoices,
552
+ buildUcxChoices: buildUcxChoices,
553
+ allocateParkedDirName: allocateParkedDirName,
554
+ writeCodexEnvDemo: writeCodexEnvDemo,
555
+ validateParkName: validateParkName,
556
+ killCodexProcesses: killCodexProcesses,
557
+ assertCanParkCurrent: assertCanParkCurrent,
558
+ switchCodexAccount: switchCodexAccount,
559
+ removeCurrentAccount: removeCurrentAccount,
560
+ };
561
+
562
+ module.exports = runUcx;
package/main.js CHANGED
@@ -6,7 +6,7 @@ var path = require('path');
6
6
  var inquirer = require('inquirer');
7
7
  var packageJson = require('./package.json');
8
8
 
9
- var VALID_TARGETS = ['jcc', 'jcx', 'weds'];
9
+ var VALID_TARGETS = ['jcc', 'jcx', 'weds', 'ucx', 'jgrok'];
10
10
 
11
11
  /**
12
12
  * 打印 base_parts_ai 入口命令的中文帮助信息
@@ -17,10 +17,12 @@ function printUsage() {
17
17
  'base_parts_ai 入口工具',
18
18
  '',
19
19
  '用法:',
20
- ' base_parts_ai 选择 jcc、jcx 或 weds 后继续执行',
20
+ ' base_parts_ai 选择 jcc、jcx、weds、ucxjgrok 后继续执行',
21
21
  ' base_parts_ai jcc [参数...] 执行 Claude Code 配置工具',
22
22
  ' base_parts_ai jcx [参数...] 执行 Codex 配置工具',
23
23
  ' base_parts_ai weds [参数...] 执行 weds Claude Code 配置工具',
24
+ ' base_parts_ai ucx [参数...] 通过目录改名切换 Codex 账号',
25
+ ' base_parts_ai jgrok [参数...] 注入 ~/.grok/.env 后启动 grok',
24
26
  ' base_parts_ai --help 显示入口帮助',
25
27
  ' base_parts_ai --version 显示当前版本',
26
28
  '',
@@ -56,13 +58,13 @@ function resolveTarget(args) {
56
58
 
57
59
  return {
58
60
  type: 'error',
59
- message: '参数无效:有参数时必须先明确指定 jcc、jcx 或 weds。',
61
+ message: '参数无效:有参数时必须先明确指定 jcc、jcx、weds、ucxjgrok。',
60
62
  };
61
63
  }
62
64
 
63
65
  /**
64
66
  * 让用户选择要继续执行的配置工具
65
- * @returns {Promise<string>} jcc、jcx 或 weds
67
+ * @returns {Promise<string>} jcc、jcx、weds、ucxjgrok
66
68
  */
67
69
  async function selectTarget() {
68
70
  var answer = await inquirer.prompt([{
@@ -73,6 +75,8 @@ async function selectTarget() {
73
75
  { name: 'jcc - Claude Code 配置工具', value: 'jcc' },
74
76
  { name: 'jcx - Codex 配置工具', value: 'jcx' },
75
77
  { name: 'weds - weds Claude Code 配置工具', value: 'weds' },
78
+ { name: 'ucx - 切换 Codex 账号', value: 'ucx' },
79
+ { name: 'jgrok - 带代理环境启动 grok', value: 'jgrok' },
76
80
  ],
77
81
  }]);
78
82
  return answer.target;
@@ -80,7 +84,7 @@ async function selectTarget() {
80
84
 
81
85
  /**
82
86
  * 获取当前包内目标 CLI 脚本路径
83
- * @param {string} target jcc、jcx 或 weds
87
+ * @param {string} target jcc、jcx、weds、ucxjgrok
84
88
  * @returns {string}
85
89
  */
86
90
  function getTargetScriptPath(target) {
@@ -88,8 +92,8 @@ function getTargetScriptPath(target) {
88
92
  }
89
93
 
90
94
  /**
91
- * 加载当前包内的 jcc/jcx/weds 入口函数
92
- * @param {string} target jcc、jcx 或 weds
95
+ * 加载当前包内的 jcc/jcx/weds/ucx/jgrok 入口函数
96
+ * @param {string} target jcc、jcx、weds、ucxjgrok
93
97
  * @returns {Function}
94
98
  */
95
99
  function loadTargetCli(target) {
@@ -97,8 +101,8 @@ function loadTargetCli(target) {
97
101
  }
98
102
 
99
103
  /**
100
- * 同进程执行当前包内的 jcc/jcx/weds 入口,并临时注入跳过自身升级的环境变量
101
- * @param {string} target jcc、jcx 或 weds
104
+ * 同进程执行当前包内的 jcc/jcx/weds/ucx/jgrok 入口,并临时注入跳过自身升级的环境变量
105
+ * @param {string} target jcc、jcx、weds、ucxjgrok
102
106
  * @param {Array<string>} args 透传给目标入口的参数
103
107
  * @param {Function} loader 测试用入口加载函数,默认 loadTargetCli
104
108
  * @returns {Promise<number>} 退出码
@@ -111,8 +115,8 @@ async function runTarget(target, args, loader) {
111
115
  if (typeof runCli !== 'function') {
112
116
  throw new Error(target + ' 入口没有导出可调用函数');
113
117
  }
114
- await runCli(args || []);
115
- return 0;
118
+ var result = await runCli(args || []);
119
+ return typeof result === 'number' ? result : 0;
116
120
  } finally {
117
121
  if (typeof oldSkipSelfUpdate === 'undefined') {
118
122
  delete process.env.BASE_PARTS_AI_SKIP_SELF_UPDATE;
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "base_parts_ai",
3
- "version": "1.0.55",
3
+ "version": "1.0.57",
4
4
  "description": "jaskle base_parts_ai",
5
5
  "main": "./main.js",
6
6
  "registry": true,
7
7
  "directories": {},
8
8
  "scripts": {
9
- "test": "node ./test_main.js && node ./test_jcc.js && node ./test_jcx.js",
9
+ "test": "node ./test_project_md_rename.js && node ./test_main.js && node ./test_jcc.js && node ./test_jcx.js && node ./test_ucx.js && node ./test_jgrok.js",
10
10
  "jcc": "node ./bin/jcc.js",
11
11
  "jcx": "node ./bin/jcx.js",
12
12
  "weds": "node ./bin/weds.js",
13
+ "ucx": "node ./bin/ucx.js",
14
+ "jgrok": "node ./bin/jgrok.js",
13
15
  "base_parts_ai": "node ./main.js",
14
16
  "__postinstall": "node ./bin/postinstall.js"
15
17
  },
@@ -17,7 +19,9 @@
17
19
  "base_parts_ai": "./main.js",
18
20
  "jcc": "./bin/jcc.js",
19
21
  "jcx": "./bin/jcx.js",
20
- "weds": "./bin/weds.js"
22
+ "weds": "./bin/weds.js",
23
+ "ucx": "./bin/ucx.js",
24
+ "jgrok": "./bin/jgrok.js"
21
25
  },
22
26
  "files": [
23
27
  "main.js",