Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
6.6 KiB
JavaScript
215 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* switch-agent.mjs — Switch the active cc-connect agent.
|
|
*
|
|
* Usage:
|
|
* node scripts/switch-agent.mjs <agent> # Switch to agent (codex, claudecode, gemini)
|
|
* node scripts/switch-agent.mjs --status # Show current active agent
|
|
*
|
|
* Strategy:
|
|
* 1. Try HTTP API (dev server has write permissions to ~/.cc-connect/)
|
|
* 2. Fall back to direct file manipulation if HTTP fails
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
import { execSync } from 'child_process';
|
|
|
|
const SUPPORTED_AGENTS = ['codex', 'claudecode', 'gemini'];
|
|
const ALIASES = { claude: 'claudecode', cc: 'claudecode', gem: 'gemini', cx: 'codex' };
|
|
|
|
// Read dev server info to get the port
|
|
function getDevServerPort() {
|
|
const candidates = [
|
|
path.join(process.cwd(), '.axhub', 'make', '.dev-server-info.json'),
|
|
path.resolve(import.meta.dirname, '..', '.axhub', 'make', '.dev-server-info.json'),
|
|
];
|
|
for (const p of candidates) {
|
|
try {
|
|
const info = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
if (info?.port) return info.port;
|
|
} catch { /* ignore */ }
|
|
}
|
|
return 51720; // default
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// HTTP-based switching (preferred — dev server has write permissions)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function httpSwitch(target) {
|
|
const port = getDevServerPort();
|
|
const url = `http://127.0.0.1:${port}/api/cc-connect/switch-agent`;
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ agent: target }),
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
return { ok: true, detail: data.message || `已切换到 ${target}` };
|
|
}
|
|
return { ok: false, detail: data.error || '切换失败' };
|
|
}
|
|
|
|
async function httpStatus() {
|
|
const port = getDevServerPort();
|
|
const url = `http://127.0.0.1:${port}/api/cc-connect/active-agent`;
|
|
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
return await res.json();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// File-based switching (fallback — requires write permissions to ~/.cc-connect/)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const CC_CONNECT_DIR = path.join(os.homedir(), '.cc-connect');
|
|
const STATE_PATH = path.join(CC_CONNECT_DIR, 'axhub-weixin-state.json');
|
|
const CONFIG_PATH = path.join(CC_CONNECT_DIR, 'config.toml');
|
|
|
|
function readState() {
|
|
if (!fs.existsSync(STATE_PATH)) return null;
|
|
try {
|
|
const parsed = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
|
|
if (parsed?.version === 1 && parsed?.activeAgent && parsed?.weixinOptions?.token) {
|
|
return parsed;
|
|
}
|
|
} catch { /* ignore */ }
|
|
return null;
|
|
}
|
|
|
|
function escapeToml(value) {
|
|
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
|
}
|
|
|
|
function fileSwitch(target) {
|
|
const state = readState();
|
|
if (!state) {
|
|
return { ok: false, detail: '未找到微信绑定状态,请先在管理后台扫码绑定' };
|
|
}
|
|
if (state.activeAgent === target) {
|
|
return { ok: true, detail: `当前已是 ${target},无需切换` };
|
|
}
|
|
|
|
state.activeAgent = target;
|
|
fs.mkdirSync(CC_CONNECT_DIR, { recursive: true });
|
|
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf8');
|
|
|
|
const optionLines = Object.entries(state.weixinOptions)
|
|
.map(([k, v]) => `${k} = "${escapeToml(String(v))}"`)
|
|
.join('\n');
|
|
|
|
const config = `# Auto-generated by Axhub Make
|
|
# language = "zh"
|
|
|
|
[log]
|
|
level = "info"
|
|
|
|
[[projects]]
|
|
name = "axhub-${escapeToml(target)}"
|
|
|
|
[projects.agent]
|
|
type = "${escapeToml(target)}"
|
|
|
|
[projects.agent.options]
|
|
work_dir = "${escapeToml(state.workDir)}"
|
|
mode = "yolo"
|
|
|
|
[[projects.platforms]]
|
|
type = "weixin"
|
|
|
|
[projects.platforms.options]
|
|
${optionLines}
|
|
`;
|
|
|
|
fs.writeFileSync(CONFIG_PATH, config, 'utf8');
|
|
|
|
const ccCommand = process.platform === 'win32' ? 'cc-connect.cmd' : 'cc-connect';
|
|
try { execSync(`${ccCommand} daemon stop`, { stdio: 'ignore', timeout: 10000 }); } catch { /* */ }
|
|
try { execSync(`${ccCommand} daemon uninstall`, { stdio: 'ignore', timeout: 10000 }); } catch { /* */ }
|
|
try {
|
|
execSync(`${ccCommand} daemon install --config "${CONFIG_PATH}"`, { stdio: 'inherit', timeout: 20000 });
|
|
return { ok: true, detail: `已切换到 ${target}` };
|
|
} catch (error) {
|
|
return { ok: false, detail: `配置已更新,但 daemon 重启失败: ${error.message}` };
|
|
}
|
|
}
|
|
|
|
function fileStatus() {
|
|
const state = readState();
|
|
if (state) {
|
|
return { activeAgent: state.activeAgent, configuredAgents: state.configuredAgents };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.length === 0 || args[0] === '--help') {
|
|
console.log('用法: node scripts/switch-agent.mjs <agent>');
|
|
console.log(`可用: ${SUPPORTED_AGENTS.join(', ')}`);
|
|
console.log('别名: claude→claudecode, gem→gemini, cx→codex');
|
|
console.log(' node scripts/switch-agent.mjs --status');
|
|
process.exit(0);
|
|
}
|
|
|
|
if (args[0] === '--status') {
|
|
const data = fileStatus();
|
|
if (data) {
|
|
console.log(JSON.stringify(data, null, 2));
|
|
} else {
|
|
try {
|
|
const httpData = await httpStatus();
|
|
console.log(JSON.stringify(httpData, null, 2));
|
|
} catch {
|
|
console.log('未找到微信绑定状态');
|
|
}
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
let target = args[0].toLowerCase();
|
|
if (ALIASES[target]) target = ALIASES[target];
|
|
|
|
if (!SUPPORTED_AGENTS.includes(target)) {
|
|
console.error(`❌ 不支持的 agent: ${target}`);
|
|
console.error(`可用: ${SUPPORTED_AGENTS.join(', ')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Strategy: try file-based first, fall back to HTTP API
|
|
try {
|
|
const result = fileSwitch(target);
|
|
if (result.ok) {
|
|
console.log(`✅ ${result.detail}`);
|
|
process.exit(0);
|
|
} else {
|
|
console.error(`❌ 本地切换失败: ${result.detail}`);
|
|
console.log('尝试 HTTP API 切换...');
|
|
}
|
|
} catch (fileError) {
|
|
console.log(`本地文件操作受限 (${fileError.message}),尝试 HTTP API 切换...`);
|
|
}
|
|
|
|
// Fallback to HTTP API
|
|
try {
|
|
const result = await httpSwitch(target);
|
|
if (result.ok) {
|
|
console.log(`✅ ${result.detail}`);
|
|
} else {
|
|
console.error(`❌ ${result.detail}`);
|
|
process.exit(1);
|
|
}
|
|
} catch (httpError) {
|
|
console.error(`❌ HTTP API 也不可达: ${httpError.message}`);
|
|
console.error('请在管理后台的微信连接对话框中进行切换');
|
|
process.exit(1);
|
|
}
|