Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions plugins/git-worktree-cockpit/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 更新日志

## 0.1.0

- 引入只读 Git 工作树、分支及状态快照。
- 所有写操作仅生成只读预案,不执行 Git 变更。
- 新增只读取当前界面授权仓库的智能体/MCP 快照工具;其他工作树不执行 `status`,响应不暴露绝对路径。
- 收紧空对象工具输入,并统一授权仓库、Git 解析与检查失败时的撤权及脱敏错误。
- 将插件界面、状态提示、对话框与 Markdown 快照完整本地化为简体中文。
- 为未压缩 `dist` 增加递归 14.5 MB(14,500,000 字节)大小门禁,并在验证时打印精确字节数。
- 在真实 Git 检查前后复验包含 ctime 的仓库身份,任一步失败都会撤销授权,MCP 错误不包含路径。
- 复制、保存与 MCP 使用稳定匿名的可分享投影;Markdown 转义 HTML/表格元字符,保存以跨平台“仅新建”策略拒绝覆盖已有文件。
19 changes: 19 additions & 0 deletions plugins/git-worktree-cockpit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Git 工作树驾驶舱

v0.1 是强只读插件。它只在用户选择并获得短期授权的普通 Git 仓库中安全探测 Git,通过固定的 `git worktree list` 与 `status --porcelain` 参数读取状态,导出 Markdown/JSON 快照;裸仓库暂不支持。

不会执行暂存、清理、重置、移除、强制操作,也不会创建或删除工作树。界面里的创建入口只会给出只读预案,明确不会运行命令。所有 Git 可执行文件必须是已验证的绝对路径,调用始终为 `shell:false`。

执行 Git 前后都会重新核对已授权仓库的 realpath 与包含 ctime 的文件身份,并使用无可选锁、禁用 fsmonitor 的只读参数;任一步失败都会撤销授权。Git 不能绑定一个已打开的仓库目录描述符执行,因此同账户恶意并发替换仍是平台级边界;插件不将此描述为完全无竞态。

## 验证

`npm test && npm run build`

只读 Git 参数、三平台路径与可执行文件候选、生命周期边界、源码/产物一致性和 Chromium 渲染已验证;构建会递归统计未压缩 `dist`、打印精确字节数,并在超过 14.5 MB(14,500,000 字节)时失败。真实 Windows、macOS、Linux ZTools 宿主加载与对话框尚未测试,Windows/Linux 的 Git 进程执行仍属于契约测试。

## 智能体 / MCP

ZTools 2.4+ 会把清单短名 `snapshot_approved` 暴露为 `git_worktree_cockpit_snapshot_approved`。智能体只能读取用户最近在插件界面选择的当前仓库授权;工具没有路径、授权 ID、Git 可执行文件或操作参数,也不提供任何写操作。旧宿主没有 `registerTool` 时仍可正常使用原有界面。

MCP 快照会在 Git 检查前后重新验证授权仓库,只对该仓库自身执行 `status`。虽然 `git worktree list` 必然读取仓库记录的工作树元数据,但不会再对未授权的其他工作树执行命令。MCP、复制与保存共用独立的可分享投影:仓库、工作树和分支使用稳定匿名标签,只保留布尔状态、超前/落后与变更条目数量,不返回名称、目录结构、用户名、变更文件、提交标识、绝对路径或原始错误文本。Markdown 会转义 HTML 与表格元字符。保存采用跨平台的原子“仅新建”策略,明确拒绝覆盖已有路径。处理器会拒绝任何字段、Symbol、访问器和异常原型;授权仓库失效或 Git 检查失败时会撤销授权并返回稳定错误。ZTools MCP 的 1 MiB 请求体边界下,本工具只接受空对象。
28 changes: 28 additions & 0 deletions plugins/git-worktree-cockpit/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Git 工作树驾驶舱</title><style>
:root{color-scheme:dark;--void:#10142c;--panel:#1a2141;--line:#33406b;--green:#66e5a7;--orange:#ffac52;--text:#edf2ff;--muted:#aebada}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 80% 0,#2c3564,transparent 32rem),var(--void);color:var(--text);font:14px system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.app{max-width:1280px;margin:auto;padding:28px}.top{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;border-bottom:1px solid var(--line);padding-bottom:20px}.eyebrow{font:700 11px ui-monospace,monospace;color:var(--green);letter-spacing:.14em}h1{margin:5px 0 0;font-size:37px;letter-spacing:-.05em}.subtitle{color:var(--muted);max-width:440px;line-height:1.55}.subtitle,.notice{overflow-wrap:break-word;text-wrap:pretty}.deck{display:grid;grid-template-columns:260px 1fr;gap:18px;margin-top:18px}.card{background:#1a2141;border:1px solid var(--line);border-radius:12px}.rail,.main{padding:18px}.rail h2,.main h2{font:700 12px ui-monospace,monospace;letter-spacing:.11em;color:var(--muted)}.button{border:1px solid #487b6b;border-radius:7px;background:#173b38;color:var(--green);padding:9px 11px;cursor:pointer;margin:5px 4px 0 0;white-space:normal;overflow-wrap:break-word}.button:hover{border-color:var(--green)}.notice{font:12px/1.6 ui-monospace,monospace;color:var(--muted);margin-top:20px}.graph{height:90px;display:flex;gap:26px;align-items:center;padding:8px 18px;background:#111632;border-radius:9px;overflow:hidden}.node{width:14px;height:14px;border-radius:50%;background:var(--green);box-shadow:0 0 0 5px #224a4a;position:relative;flex:none}.node:after{content:"";position:absolute;width:40px;height:2px;background:#66e5a7;left:14px;top:6px}.node:nth-child(3n){background:var(--orange);box-shadow:0 0 0 5px #614222}.node:last-child:after{display:none}.table{margin-top:15px;border:1px solid var(--line);border-radius:9px;overflow:hidden}.row{display:grid;grid-template-columns:1.6fr 1fr .8fr .6fr;gap:8px;padding:12px 14px;border-top:1px solid var(--line);font:12px ui-monospace,monospace;overflow-wrap:anywhere}.row:first-child{border-top:0;color:var(--muted);font-weight:700}.dirty{color:var(--orange)}.clean{color:var(--green)}button:focus-visible{outline:2px solid var(--green);outline-offset:2px}@media(max-width:860px){.top{align-items:start;flex-direction:column}}@media(max-width:740px){.app{padding:16px}h1{font-size:29px}.deck{grid-template-columns:1fr}.row{grid-template-columns:1fr 1fr}}@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
</style></head><body><main class="app"><header class="top"><div><div class="eyebrow">只读工作台 / v0.1</div><h1>Git 工作树驾驶舱</h1></div><p class="subtitle" id="status">选择一个本地 Git 仓库。所有命令固定、使用绝对路径执行且 <code>shell:false</code>;v0.1 不会改动任何工作树。</p></header><section class="deck"><aside class="card rail"><h2>授权仓库</h2><button class="button" id="choose">选择仓库</button><button class="button" id="inspect" disabled>只读检查</button><button class="button" id="copy" disabled>复制 Markdown</button><button class="button" id="saveMd" disabled>保存 Markdown</button><button class="button" id="saveJson" disabled>保存 JSON</button><button class="button" id="dry">生成创建预案</button><p class="notice">仅显示:工作树、分支、HEAD、超前/落后、未提交改动、锁定与可清理状态。不会执行暂存、重置、清理、移除或强制操作。</p></aside><article class="card main"><h2>分支图</h2><div class="graph" aria-label="分支图示"><i class="node"></i><i class="node"></i><i class="node"></i><i class="node"></i><i class="node"></i><i class="node"></i></div><div class="table" id="table"><div class="row"><span>工作树</span><span>分支</span><span>同步</span><span>状态</span></div></div></article></section></main><script>
const api=window.gitWorktreeCockpit;
const status=document.querySelector('#status');
const table=document.querySelector('#table');
const chooseButton=document.querySelector('#choose');
const actionButtons=['inspect','copy','saveMd','saveJson'].map(id=>document.querySelector('#'+id));
let grantId=null;
let rendererEpoch=0;
function cell(value,className){const span=document.createElement('span');span.textContent=String(value??'—');if(className)span.className=className;return span}
function visibleCode(code){return({DIALOG_UNAVAILABLE:'当前 ZTools 版本不支持目录选择对话框。',CANCELED:'已取消选择。',REPOSITORY_UNAVAILABLE:'无法访问或验证所选仓库,请重新选择。',REPOSITORY_INVALID:'所选目录不是可用的 Git 仓库目录。',DESTINATION_EXISTS:'不能覆盖已有文件,请选择新的文件名。',SAVE_FAILED:'无法保存快照,请重新选择保存位置。',COPY_FAILED:'无法复制快照,请重试。',SESSION_ENDED:'插件会话已结束,请重新打开后操作。'})[code]||'操作未完成。'}
function visibleError(error){return visibleCode(error&&error.code)}
function renderHeader(){table.replaceChildren();const header=document.createElement('div');header.className='row';for(const value of ['工作树','分支','同步','状态'])header.append(cell(value));table.append(header)}
function render(data){renderHeader();for(const item of data.worktrees){const s=item.status||{};const row=document.createElement('div');row.className='row';row.append(cell(item.path),cell(item.branch||(item.detached?'游离':'—')),cell('+'+(s.ahead||0)+' / −'+(s.behind||0)),cell(item.locked?'已锁定':item.prunable?'可清理':item.statusUnavailable?'不可用':s.dirty?'有改动':'干净',s.dirty?'dirty':'clean'));table.append(row)}}
function enabled(value){actionButtons.forEach(button=>button.disabled=!value)}
function isCurrent(epoch){return epoch===rendererEpoch}
function resetRenderer(){rendererEpoch+=1;grantId=null;enabled(false);chooseButton.disabled=false;renderHeader();status.textContent='选择一个本地 Git 仓库。所有命令固定、使用绝对路径执行且 shell:false;v0.1 不会改动任何工作树。'}
// services.cjs owns the single ZTools onPluginOut subscription and sends only
// this path-free lifecycle event to the renderer.
window.addEventListener('git-worktree-cockpit-session-ended',resetRenderer);
async function runCurrent(button,operation,onSuccess,onFailure){const epoch=rendererEpoch;button.disabled=true;try{const result=await operation();if(!isCurrent(epoch))return;onSuccess(result)}catch(error){if(!isCurrent(epoch))return;onFailure(error)}finally{if(!isCurrent(epoch))return;button.disabled=false}}
chooseButton.onclick=()=>runCurrent(chooseButton,()=>api.chooseRepository(),result=>{if(!result.ok){status.textContent=visibleCode(result.code);return}grantId=result.grantId;enabled(true);status.textContent='已授权 '+result.repository+',授权 30 分钟。'},error=>{status.textContent='授权失败:'+visibleError(error)});
document.querySelector('#inspect').onclick=function(){const id=grantId;return runCurrent(this,()=>api.inspectGrant(id),result=>{render(result);status.textContent='已只读读取 '+result.worktrees.length+' 个工作树。'},error=>{status.textContent='无法读取:'+visibleError(error)})};
document.querySelector('#copy').onclick=function(){const id=grantId;return runCurrent(this,()=>api.copySnapshot(id,'markdown'),()=>{status.textContent='Markdown 已复制。'},error=>{status.textContent='复制失败:'+visibleError(error)})};
for(const [id,format]of[['saveMd','markdown'],['saveJson','json']])document.querySelector('#'+id).onclick=function(){const grant=id&&grantId;return runCurrent(this,()=>api.saveSnapshot(grant,format),result=>{status.textContent=result.canceled?'已取消保存':'已保存 '+result.fileName},error=>{status.textContent='保存失败:'+visibleError(error)})};
document.querySelector('#dry').onclick=()=>{const plan=api.dryPlan('create');status.textContent=plan.message};
</script></body></html>
1 change: 1 addition & 0 deletions plugins/git-worktree-cockpit/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions plugins/git-worktree-cockpit/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions plugins/git-worktree-cockpit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "git-worktree-cockpit",
"version": "0.1.0",
"private": true,
"description": "Read-only local Git worktree cockpit for ZTools.",
"scripts": { "test": "node --test tests/*.test.cjs", "build": "npm test && node scripts/build.mjs && node scripts/verify-dist.mjs", "verify-dist": "node scripts/verify-dist.mjs" },
"engines": { "node": ">=16" }
}
41 changes: 41 additions & 0 deletions plugins/git-worktree-cockpit/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "git-worktree-cockpit",
"title": "Git 工作树驾驶舱",
"description": "本地只读查看 Git 工作树、分支与未提交状态,生成可分享快照。",
"version": "0.1.0",
"author": "harris",
"main": "index.html",
"preload": "preload/services.cjs",
"logo": "logo.svg",
"platform": [
"darwin",
"win32",
"linux"
],
"categories": [
"development"
],
"features": [
{
"code": "git-worktree-cockpit-open",
"explain": "查看已授权 Git 仓库的工作树状态",
"icon": "logo.svg",
"cmds": [
"查看 Git 工作树",
"Git 工作树状态",
"分支驾驶舱"
]
}
],
"tools": {
"snapshot_approved": {
"title": "读取已授权 Git 工作树快照",
"description": "只读检查用户当前在插件界面授权的 Git 仓库,不接受路径,不访问或返回其他工作树的绝对路径。",
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
}
}
Loading
Loading