Files
deta-agent-plugins/plugins/studio/bin/deta-cli.mjs
T
ringo 5778962ba5
CI / contract (push) Has been cancelled
fix(plugin): 产品档位改读插件清单,修缓存布局下把版本号当产品名
症状(实测):宿主缓存布局是 <市场>/<插件>/<版本>/,deta-cli 原先用「自身目录名的上一级」推断产品名,于是 status 显示工作区 ~/.deta/0.3.3/workspace,doctor 会以 --profile 0.3.3 拉起服务并真的创建该目录。

修复:detectProfile 优先读 .codex-plugin/plugin.json 的 name(开发态与缓存态都对),DETA_PROFILE 仍可覆盖,清单不可读才回落目录名。studio 0.3.4 / writer 0.1.2。

验证:重装 0.3.4 后 status 显示 /Users/ringo/.deta/studio/workspace,doctor 以 --profile studio 握手成功 37 工具。
2026-09-20 12:13:10 +08:00

165 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* bin/deta-cli.mjs — DETA 命令行门面(登录 / 自检)
*
* 为什么需要:登录助手原先只在插件目录里(`<插件缓存>/bin/deta-login.mjs`),
* 普通用户既不知道插件装在哪,也没有稳定入口;工具回执里让用户「跑登录助手」时,
* 给不出一个可复制的命令。启动器会把本文件注册为 `~/.deta/bin/deta`(幂等维护),
* 于是各处统一口径就变成:
*
* ~/.deta/bin/deta login # 登录 deta.city(写 ~/.deta/credentials.json 与 env)
* ~/.deta/bin/deta status # 看凭据来源 / 运行时 / 工作区
* ~/.deta/bin/deta doctor # 端到端自检(运行时、凭据、编译服务、MCP 握手)
*
* 纪律:本文件**永不打印令牌**,只报告来源与长度;密码/验证码一律由 deta-login.mjs
* 在用户自己的终端里交互读取(不回显)。
*/
import { spawn } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join, resolve, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const HOME = homedir();
const DETA_DIR = join(HOME, '.deta');
/** 产品档位:优先读插件清单的 name——开发态是 `plugins/studio`,而宿主缓存布局是
* `<市场>/<插件>/<版本>/`,从路径取目录名会把版本号当成产品名(实测踩过)。 */
function detectProfile() {
if (process.env.DETA_PROFILE) return process.env.DETA_PROFILE;
try {
const manifest = JSON.parse(readFileSync(join(HERE, '..', '.codex-plugin', 'plugin.json'), 'utf8'));
if (typeof manifest?.name === 'string' && manifest.name) return manifest.name;
} catch { /* 清单不可读时回落目录名(仅开发态正确) */ }
return basename(join(HERE, '..'));
}
const PROFILE = detectProfile();
function readToken() {
if (process.env.DETA_TOKEN) return { source: '环境变量 DETA_TOKEN', token: process.env.DETA_TOKEN };
const envPath = join(DETA_DIR, 'env');
if (existsSync(envPath)) {
const m = /DETA_TOKEN=('([^']*)'|"([^"]*)"|(\S+))/.exec(readFileSync(envPath, 'utf8'));
const token = (m?.[2] ?? m?.[3] ?? m?.[4] ?? '').trim();
if (token) return { source: envPath, token };
}
const credPath = join(DETA_DIR, 'credentials.json');
if (existsSync(credPath)) {
try {
const c = JSON.parse(readFileSync(credPath, 'utf8'));
if (typeof c.token === 'string' && c.token) return { source: credPath, token: c.token, meta: c };
} catch { /* 忽略损坏的凭据文件,按未登录处理 */ }
}
return { source: '', token: '' };
}
function cmdLogin(argv) {
const loginScript = join(HERE, 'deta-login.mjs');
if (!existsSync(loginScript)) {
console.error(`✗ 找不到登录助手:${loginScript}`);
process.exit(1);
}
// 直接交给登录助手,stdio 全继承:密码由用户在终端输入(本进程不经手)
const child = spawn(process.execPath, [loginScript, ...argv], { stdio: 'inherit' });
child.on('exit', (code) => process.exit(code ?? 1));
}
function cmdStatus() {
const { source, token, meta } = readToken();
console.log(`凭据:${token ? `已存在(来源 ${source},长度 ${token.length})` : '未登录'}`);
if (meta?.username) console.log(`账号:${meta.username}(登录于 ${meta.loginAt ?? '未知'})`);
console.log(`工作区:${process.env.DETA_WORKSPACE || join(DETA_DIR, PROFILE, 'workspace')}`);
const runtime = resolveRuntime();
console.log(`运行时:${runtime || '未找到(需要本机引擎包或 DETA_SERVER_ROOT)'}`);
if (!token) console.log('\n未登录时云类工具(项目列表 / 同步 / 打开 Studio)会返回 auth_required;跑 `deta login` 即可。');
}
/** 运行时解析,与 bin/deta-mcp.sh 保持同一顺序 */
function resolveRuntime() {
const candidates = [
process.env.DETA_SERVER_ROOT || '',
join(DETA_DIR, 'runtime'),
join(HOME, 'Dev/ant/ant-copilot'),
].filter(Boolean);
for (const c of candidates) {
if (existsSync(join(c, 'dist/deta-mcp.mjs')) || existsSync(join(c, 'dist/src/headless/mcp-server.js'))) return c;
}
return '';
}
function smokeMcp(launcher, profile) {
return new Promise((resolveP) => {
const child = spawn('bash', [launcher, '--profile', profile], { stdio: ['pipe', 'pipe', 'pipe'] });
let out = '';
let err = '';
let done = false;
const finish = (verdict) => { if (!done) { done = true; child.kill(); resolveP(verdict); } };
child.stdout.on('data', (d) => { out += d; });
child.stderr.on('data', (d) => { err += d; });
child.on('error', (e) => finish({ ok: false, detail: `无法启动:${e.message}` }));
const send = (o) => { try { child.stdin.write(JSON.stringify(o) + '\n'); } catch { /* 进程已退出 */ } };
send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'deta-doctor', version: '1' } } });
setTimeout(() => send({ jsonrpc: '2.0', method: 'notifications/initialized' }), 150);
setTimeout(() => send({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }), 400);
setTimeout(() => {
const lines = out.split('\n').filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
const init = lines.find((x) => x.id === 1)?.result;
const list = lines.find((x) => x.id === 2)?.result;
const tokenLine = err.split('\n').find((l) => l.includes('云令牌')) || '';
if (!init) return finish({ ok: false, detail: 'MCP 未应答 initialize' });
if (!list) return finish({ ok: false, detail: 'MCP 未应答 tools/list' });
finish({ ok: true, server: init.serverInfo, tools: (list.tools ?? []).length, tokenLine });
}, 5000);
});
}
async function cmdDoctor() {
let failed = 0;
const say = (ok, label, detail) => { console.log(`${ok ? '✓' : '✗'} ${label}${detail ? ` —— ${detail}` : ''}`); if (!ok) failed += 1; };
const runtime = resolveRuntime();
say(Boolean(runtime), '领域服务运行时', runtime || '未找到:装本机引擎包到 ~/.deta/runtime,或设 DETA_SERVER_ROOT');
const { source, token } = readToken();
say(Boolean(token), '云凭据', token ? `已存在(来源 ${source},长度 ${token.length})` : '未登录:跑 `~/.deta/bin/deta login`');
const gsa = process.env.GSA_BASE_URL || 'http://localhost:8000';
let gsaOk = false;
try {
const res = await fetch(`${gsa}/health`, { signal: AbortSignal.timeout(4000) });
gsaOk = res.ok;
} catch { gsaOk = false; }
say(gsaOk, '编译服务 GSA', gsaOk ? `${gsa}/health 通` : `${gsa} 不可达(编译/统计类工具会失败)`);
if (runtime) {
const launcher = join(HERE, 'deta-mcp.sh');
const profile = PROFILE;
const r = await smokeMcp(launcher, profile);
say(r.ok, `MCP 握手(--profile ${profile})`, r.ok ? `${r.tools} 个工具 · ${r.server?.name ?? '?'}${r.tokenLine ? ` · ${r.tokenLine.trim()}` : ''}` : r.detail);
}
console.log(failed ? `\n自检完成:${failed} 项需要处理。` : '\n自检完成:全部通过。');
process.exit(failed ? 1 : 0);
}
const [cmd = 'help', ...rest] = process.argv.slice(2);
switch (cmd) {
case 'login': cmdLogin(rest); break;
case 'status': cmdStatus(); break;
case 'doctor': await cmdDoctor(); break;
case 'help':
case '--help':
case '-h':
console.log(`DETA 命令行(插件自动注册为 ~/.deta/bin/deta)
deta login [--identifier <账号>] [--email-code <邮箱>] 登录 deta.city(凭据写入 ~/.deta/)
deta status 查看凭据来源 / 运行时 / 工作区
deta doctor 端到端自检(运行时、凭据、GSA、MCP 握手)
登录后开新会话即可让宿主读到凭据(无需 source / launchctl / 重启应用)。`);
break;
default:
console.error(`未知命令:${cmd}(可用:login / status / doctor)`);
process.exit(2);
}