Files
deta-agent-plugins/plugins/studio/bin/deta-cli.mjs
T
ringo 54924cd4fc
CI / contract (push) Has been cancelled
feat(plugin): 稳定命令行入口 ~/.deta/bin/deta(login / status / doctor)
为什么需要:登录助手原先只在插件目录里(宿主缓存路径 <缓存>/bin/deta-login.mjs),普通用户不知道插件装在哪;工具回执让用户「跑登录助手」时给不出可复制的命令。

实现:新增 bin/deta-cli.mjs(login 转交登录助手、status 看凭据来源/运行时/工作区、doctor 自检运行时+凭据+GSA health+MCP 握手,永不打印令牌);启动器每次拉起时幂等注册 ~/.deta/bin/deta(路径随插件更新自动刷新,幂等比较避免重复写);技能与三份 README 的登录口径统一为 \`~/.deta/bin/deta login\`。版本 studio 0.3.3 / writer 0.1.1。

实测:shim 生成与 help/status/doctor 均符合预期,doctor 准确报出唯一缺口(未登录)并握手成功 37 工具。
2026-09-20 12:10:49 +08:00

156 lines
7.5 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');
/** 产品档位:由本文件所在插件目录名决定(studio / writer),DETA_PROFILE 可覆盖 */
const PROFILE = process.env.DETA_PROFILE || basename(join(HERE, '..'));
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);
}