diff --git a/README.md b/README.md index 721f72f..653b1c2 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,12 @@ bash plugins/studio/bin/deta-mcp.sh ``` 装完即用,**没有 OAuth 登录步骤**。需要云侧能力(项目列表 / 项目同步 / 打开 Studio)时登录一次 -(助手用法见 `plugins/studio/README.md`「登录助手」): +(`~/.deta/bin/deta` 由启动器自动注册;助手用法见 `plugins/studio/README.md`「登录助手」): ```bash -node plugins/studio/bin/deta-login.mjs # 账号密码 / 邮箱验证码由你本人输入 -# 然后开新会话即可,无需 source / launchctl / 重启应用 +~/.deta/bin/deta login # 账号密码 / 邮箱验证码由你本人输入 +~/.deta/bin/deta doctor # 自检:运行时 / 凭据 / GSA / MCP 握手 +# 登录后开新会话即可,无需 source / launchctl / 重启应用 ``` ## 本地迭代 diff --git a/plugins/studio/.codex-plugin/plugin.json b/plugins/studio/.codex-plugin/plugin.json index 16142dc..e642c02 100644 --- a/plugins/studio/.codex-plugin/plugin.json +++ b/plugins/studio/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "studio", - "version": "0.3.2", + "version": "0.3.3", "description": "DETA Studio 插件:以 AML 为唯一事实源编辑建筑模型,编译为建筑模型并执行规则校验。", "author": { "name": "DETA", diff --git a/plugins/studio/README.md b/plugins/studio/README.md index 048d254..db5654e 100644 --- a/plugins/studio/README.md +++ b/plugins/studio/README.md @@ -89,11 +89,13 @@ codex plugin add studio@deta bash <插件目录>/bin/deta-mcp.sh --profile studio ``` -装完即用,**没有 OAuth 登录步骤**。需要云侧能力(项目列表 / 项目同步 / 打开 Studio)时登录一次: +装完即用,**没有 OAuth 登录步骤**。需要云侧能力(项目列表 / 项目同步 / 打开 Studio)时登录一次 +(`~/.deta/bin/deta` 由启动器自动注册,无需手工创建): ```bash -node <插件目录>/bin/deta-login.mjs # 登录 deta.city:账号密码或邮箱验证码由你本人输入 - # 写 ~/.deta/credentials.json 与 ~/.deta/env(均 0600) +~/.deta/bin/deta login # 输入账号 + 密码,或 --email-code <邮箱> 走验证码 +~/.deta/bin/deta status # 看凭据来源 / 运行时 / 工作区(不打印令牌) +~/.deta/bin/deta doctor # 端到端自检:运行时、凭据、GSA、MCP 握手 ``` 登录后**开新会话**(MCP 服务重新拉起时读到凭据)。启动器自己按 diff --git a/plugins/studio/bin/deta-cli.mjs b/plugins/studio/bin/deta-cli.mjs new file mode 100644 index 0000000..99c81ac --- /dev/null +++ b/plugins/studio/bin/deta-cli.mjs @@ -0,0 +1,155 @@ +#!/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); +} diff --git a/plugins/studio/bin/deta-mcp.sh b/plugins/studio/bin/deta-mcp.sh index c1ad240..69025b3 100755 --- a/plugins/studio/bin/deta-mcp.sh +++ b/plugins/studio/bin/deta-mcp.sh @@ -68,6 +68,20 @@ export ANT_HEADLESS_WORKSPACE="$WS" # ── 工具面:产品档位缺省全量面;DETA_SURFACE 可覆盖(如 engine)── export DETA_TOOL_SURFACE="${DETA_SURFACE:-full}" +# ── 稳定入口:把命令行门面注册为 ~/.deta/bin/deta(幂等,路径随插件更新自动刷新)── +# 插件装在宿主缓存目录,用户看不见这个路径;而用户与工具回执都需要一条可复制的命令。 +PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN_DIR="$HOME/.deta/bin" +SHIM="$BIN_DIR/deta" +SHIM_BODY="#!/bin/sh +# DETA 命令行门面(由插件启动器自动维护,勿手工编辑) +exec \"$(command -v node)\" \"$PLUGIN_ROOT/bin/deta-cli.mjs\" \"\$@\" +" +mkdir -p "$BIN_DIR" 2>/dev/null || true +if [ ! -f "$SHIM" ] || [ "$(cat "$SHIM" 2>/dev/null)" != "$SHIM_BODY" ]; then + printf '%s' "$SHIM_BODY" > "$SHIM" 2>/dev/null && chmod 0755 "$SHIM" 2>/dev/null || true +fi + # ── 云令牌:显式环境变量 → ~/.deta/env → ~/.deta/credentials.json ── # 桌面应用(Finder/Dock 启动)不会继承终端的 export,也不是每个人都有 launchctl 注入, # 因此登录助手写下的凭据必须由启动器自己解析——这是"登录一次即可用"的关键路径。 @@ -89,7 +103,7 @@ fi if [ -n "${DETA_TOKEN:-}" ]; then CLOUD_STATE="已注入(来源=${TOKEN_SOURCE})" else - CLOUD_STATE="未注入——云类工具将返回 auth_required:运行 bin/deta-login.mjs 登录后开新会话" + CLOUD_STATE="未注入——云类工具将返回 auth_required:运行 ~/.deta/bin/deta login 登录后开新会话" fi echo "[deta] 档位=${PROFILE} 运行时=${ROOT} 工作区=${WS} 工具面=${DETA_TOOL_SURFACE} 云令牌=${CLOUD_STATE}" >&2 diff --git a/plugins/studio/package-lock.json b/plugins/studio/package-lock.json index fd14feb..8cdaf84 100644 --- a/plugins/studio/package-lock.json +++ b/plugins/studio/package-lock.json @@ -1,12 +1,12 @@ { "name": "deta-studio-plugin", - "version": "0.3.2", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "deta-studio-plugin", - "version": "0.3.2", + "version": "0.3.3", "license": "Proprietary" } } diff --git a/plugins/studio/package.json b/plugins/studio/package.json index 79ef88a..eefad9a 100644 --- a/plugins/studio/package.json +++ b/plugins/studio/package.json @@ -1,6 +1,6 @@ { "name": "deta-studio-plugin", - "version": "0.3.2", + "version": "0.3.3", "description": "DETA Studio:AML 建筑模型读写与编译校验(Codex 插件 / dsh 组合包 双壳)", "type": "module", "license": "Proprietary", diff --git a/plugins/studio/skills/aml-design/SKILL.md b/plugins/studio/skills/aml-design/SKILL.md index 31fe85c..71408ad 100644 --- a/plugins/studio/skills/aml-design/SKILL.md +++ b/plugins/studio/skills/aml-design/SKILL.md @@ -112,9 +112,9 @@ Studio 就是deta.city 上的建筑性能应用。在 Codex 右侧内置浏览 - 典型节奏:`open_studio()` 拿 `url` → 面板打开 → `open_studio({ waitSeconds: 20 })` 确认 `executor_online: true` → 再调 `simulate` / `studio_command` 等执行器侧工具。 - 若返回 `auth_required` → 云令牌未注入(**本机形态没有 MCP 授权,不要去找 `codex mcp login`**): - 让用户在终端运行插件自带登录助手 `node <插件目录>/bin/deta-login.mjs`(账号与密码/验证码由用户 - 本人输入;助手写 `~/.deta/credentials.json` 与 `~/.deta/env`,启动器会自动读取——不需 source、 - 不需 launchctl、不需重启应用),随后**开新会话**重试; + 让用户在终端运行 `~/.deta/bin/deta login`(按提示输入账号与密码或邮箱验证码,由用户本人输入), + 随后**开新会话**重试;命令不存在时可回落到插件目录下的 `bin/deta-login.mjs`。 + 排查用 `~/.deta/bin/deta doctor`(一条命令自检运行时 / 凭据 / GSA / MCP 握手)。 若 `url` 报「票据无效或已过期」→ 重新调一次,不要改成手工登录。 2. 按 browser 插件技能(`control-in-app-browser`)的 Bootstrap 初始化,再取内置浏览器句柄: `const iab = await agent.browsers.get("iab");`,并先读一次 `await iab.documentation()`。 diff --git a/plugins/writer/.codex-plugin/plugin.json b/plugins/writer/.codex-plugin/plugin.json index fabafb7..214629c 100644 --- a/plugins/writer/.codex-plugin/plugin.json +++ b/plugins/writer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "writer", - "version": "0.1.0", + "version": "0.1.1", "description": "DETA Writer 插件:读取建筑模型与模拟结果,产出报告 / 文档内容。", "author": { "name": "DETA", diff --git a/plugins/writer/README.md b/plugins/writer/README.md index c8c19fa..13c889a 100644 --- a/plugins/writer/README.md +++ b/plugins/writer/README.md @@ -53,8 +53,8 @@ bash <插件目录>/bin/deta-mcp.sh --profile writer 需要云侧能力(项目列表 / 项目同步 / 打开 Studio)时登录一次: ```bash -node <插件目录>/bin/deta-login.mjs # 账号密码 / 邮箱验证码由你本人输入 - # 写 ~/.deta/credentials.json 与 ~/.deta/env(均 0600) +~/.deta/bin/deta login # 由启动器自动注册;账号密码或 --email-code <邮箱> +~/.deta/bin/deta doctor # 自检:运行时 / 凭据 / GSA / MCP 握手 ``` 登录后**开新会话**即可——启动器自己按 `DETA_TOKEN` 环境变量 → `~/.deta/env` → diff --git a/plugins/writer/bin/deta-cli.mjs b/plugins/writer/bin/deta-cli.mjs new file mode 100644 index 0000000..99c81ac --- /dev/null +++ b/plugins/writer/bin/deta-cli.mjs @@ -0,0 +1,155 @@ +#!/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); +} diff --git a/plugins/writer/bin/deta-mcp.sh b/plugins/writer/bin/deta-mcp.sh index 22ef3ca..a3b6397 100755 --- a/plugins/writer/bin/deta-mcp.sh +++ b/plugins/writer/bin/deta-mcp.sh @@ -68,6 +68,20 @@ export ANT_HEADLESS_WORKSPACE="$WS" # ── 工具面:产品档位缺省全量面;DETA_SURFACE 可覆盖(如 engine)── export DETA_TOOL_SURFACE="${DETA_SURFACE:-full}" +# ── 稳定入口:把命令行门面注册为 ~/.deta/bin/deta(幂等,路径随插件更新自动刷新)── +# 插件装在宿主缓存目录,用户看不见这个路径;而用户与工具回执都需要一条可复制的命令。 +PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN_DIR="$HOME/.deta/bin" +SHIM="$BIN_DIR/deta" +SHIM_BODY="#!/bin/sh +# DETA 命令行门面(由插件启动器自动维护,勿手工编辑) +exec \"$(command -v node)\" \"$PLUGIN_ROOT/bin/deta-cli.mjs\" \"\$@\" +" +mkdir -p "$BIN_DIR" 2>/dev/null || true +if [ ! -f "$SHIM" ] || [ "$(cat "$SHIM" 2>/dev/null)" != "$SHIM_BODY" ]; then + printf '%s' "$SHIM_BODY" > "$SHIM" 2>/dev/null && chmod 0755 "$SHIM" 2>/dev/null || true +fi + # ── 云令牌:显式环境变量 → ~/.deta/env → ~/.deta/credentials.json ── # 桌面应用(Finder/Dock 启动)不会继承终端的 export,也不是每个人都有 launchctl 注入, # 因此登录助手写下的凭据必须由启动器自己解析——这是"登录一次即可用"的关键路径。 @@ -89,7 +103,7 @@ fi if [ -n "${DETA_TOKEN:-}" ]; then CLOUD_STATE="已注入(来源=${TOKEN_SOURCE})" else - CLOUD_STATE="未注入——云类工具将返回 auth_required:运行 bin/deta-login.mjs 登录后开新会话" + CLOUD_STATE="未注入——云类工具将返回 auth_required:运行 ~/.deta/bin/deta login 登录后开新会话" fi echo "[deta] 档位=${PROFILE} 运行时=${ROOT} 工作区=${WS} 工具面=${DETA_TOOL_SURFACE} 云令牌=${CLOUD_STATE}" >&2 diff --git a/plugins/writer/package-lock.json b/plugins/writer/package-lock.json index 8783ba9..e990d61 100644 --- a/plugins/writer/package-lock.json +++ b/plugins/writer/package-lock.json @@ -1,12 +1,12 @@ { "name": "deta-writer-plugin", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "deta-writer-plugin", - "version": "0.1.0", + "version": "0.1.1", "license": "Proprietary" } } diff --git a/plugins/writer/package.json b/plugins/writer/package.json index 707c292..874b7d0 100644 --- a/plugins/writer/package.json +++ b/plugins/writer/package.json @@ -1,6 +1,6 @@ { "name": "deta-writer-plugin", - "version": "0.1.0", + "version": "0.1.1", "description": "DETA Writer:建筑性能报告与文档产出(Codex 插件 / dsh 组合包 双壳)", "type": "module", "license": "Proprietary",