From 7f23a9fe0f52c48533c05231145442bb53a9c6ca Mon Sep 17 00:00:00 2001 From: harveysang Date: Mon, 7 Sep 2026 09:26:25 +0800 Subject: [PATCH 1/4] feat(plugins): add prebuilt macOS installation flows --- .github/workflows/opengui-codex-ci.yml | 5 +- .github/workflows/opengui-codex-release.yml | 8 +- .github/workflows/workbuddy-plugin-ci.yml | 1 + docs/plans/simple-plugin-install.md | 25 +++ plugins/opengui/README.md | 34 +++- plugins/opengui/README.zh-CN.md | 28 +++- plugins/opengui/scripts/install-macos.command | 156 ++++++++++++++++++ plugins/opengui/scripts/opengui | 2 +- plugins/opengui/scripts/package.mjs | 6 +- plugins/opengui/scripts/test-installer.mjs | 62 +++++++ plugins/opengui/scripts/validate.mjs | 2 + skills/opengui-plugin-install/SKILL.md | 22 +++ workbuddy-plugin/README.md | 50 +++--- workbuddy-plugin/README.zh-CN.md | 39 +++-- workbuddy-plugin/package.json | 2 +- .../scripts/install-macos.command | 114 +++++++++++++ workbuddy-plugin/scripts/package.mjs | 8 +- workbuddy-plugin/scripts/publish.mjs | 2 +- .../scripts/test-release-installer.mjs | 38 +++++ workbuddy-plugin/scripts/validate.mjs | 3 + 20 files changed, 547 insertions(+), 60 deletions(-) create mode 100644 docs/plans/simple-plugin-install.md create mode 100755 plugins/opengui/scripts/install-macos.command create mode 100644 plugins/opengui/scripts/test-installer.mjs create mode 100644 skills/opengui-plugin-install/SKILL.md create mode 100755 workbuddy-plugin/scripts/install-macos.command create mode 100644 workbuddy-plugin/scripts/test-release-installer.mjs diff --git a/.github/workflows/opengui-codex-ci.yml b/.github/workflows/opengui-codex-ci.yml index 518c3d7..1e2d3df 100644 --- a/.github/workflows/opengui-codex-ci.yml +++ b/.github/workflows/opengui-codex-ci.yml @@ -43,7 +43,10 @@ jobs: run: pnpm check - name: Produce the allowlisted upload and archive working-directory: ${{ runner.temp }}/opengui - run: pnpm package + run: | + pnpm package + bash -n scripts/install-macos.command + node scripts/test-installer.mjs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: opengui-codex-${{ matrix.os }} diff --git a/.github/workflows/opengui-codex-release.yml b/.github/workflows/opengui-codex-release.yml index ff78013..1ab2693 100644 --- a/.github/workflows/opengui-codex-release.yml +++ b/.github/workflows/opengui-codex-release.yml @@ -65,8 +65,9 @@ jobs: version="${GITHUB_REF_NAME#opengui-codex-v}" archive="opengui-codex-$version.tar.gz" upload="opengui-codex-$version.zip" - test "$(find release-assets -maxdepth 1 -type f | wc -l | tr -d ' ')" = 4 - (cd release-assets && sha256sum -c "$archive.sha256" && sha256sum -c "$upload.sha256") + installer="opengui-codex-$version-install.command" + test "$(find release-assets -maxdepth 1 -type f | wc -l | tr -d ' ')" = 6 + (cd release-assets && sha256sum -c "$archive.sha256" && sha256sum -c "$upload.sha256" && sha256sum -c "$installer.sha256") tar -xOf "release-assets/$archive" opengui/docs/release-notes.md > "$RUNNER_TEMP/notes.md" if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then test "$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq .isDraft)" = true @@ -76,6 +77,7 @@ jobs: fi gh release upload "$GITHUB_REF_NAME" \ "release-assets/$archive" "release-assets/$archive.sha256" \ - "release-assets/$upload" "release-assets/$upload.sha256" --clobber + "release-assets/$upload" "release-assets/$upload.sha256" \ + "release-assets/$installer" "release-assets/$installer.sha256" --clobber # Keep the release draft until real-device acceptance is recorded. # Directory submission, approval, and publication are separate manual gates. diff --git a/.github/workflows/workbuddy-plugin-ci.yml b/.github/workflows/workbuddy-plugin-ci.yml index ad5088b..80cc914 100644 --- a/.github/workflows/workbuddy-plugin-ci.yml +++ b/.github/workflows/workbuddy-plugin-ci.yml @@ -48,6 +48,7 @@ jobs: cache-dependency-path: workbuddy-plugin/package-lock.json - run: npm ci - run: npm run pack:release + - run: node scripts/test-release-installer.mjs - run: npm run test:native - uses: actions/upload-artifact@v4 with: diff --git a/docs/plans/simple-plugin-install.md b/docs/plans/simple-plugin-install.md new file mode 100644 index 0000000..72034f7 --- /dev/null +++ b/docs/plans/simple-plugin-install.md @@ -0,0 +1,25 @@ +# Codex / WorkBuddy 简化安装方案 + +目标:macOS 用户下载对应宿主的安装脚本并运行一次,自动取得固定版本预构建包、校验 SHA-256、准备私有 Node、安装并给出首次使用指引。普通安装不需要源码、Git、pnpm、编译器或测试工具。 + +每个插件维护自己的安装脚本和发布资产;不共享运行时、配置、版本或设备服务。沿用 DSH 已验证的机制:版本化包、HTTPS 下载、校验、独立目录、配置备份。保留现有发布验收,不把候选产物声称为正式发布。 + +流程:GitHub Release -> 对应宿主安装脚本 -> 私有版本目录 -> 宿主原生插件安装 / WorkBuddy 配置合并。 + +- Codex:使用独立 opengui-standalone 来源,保持仓库旧 marketplace 不变;拒绝同时安装其他来源的同名插件。通过 codex plugin 命令注册、安装和核验,保留配置备份。沿用已固定的 Node 22.23.2 和独立缓存。 +- WorkBuddy:先检查宿主退出,再使用私有 Node/npm 安装校验过的 tgz(禁用安装脚本),从包内运行既有配置安装器,统一处理 MCP、Skill 和 Hooks。保留新旧包目录和回退日志。 +- 发布:为两种插件新增带版本的安装脚本及校验文件;CI 同时验证脚本、归档和安装行为。正式发布仍要求原有人工验收证据。 +- 开发验收:支持显式本地 archive + SHA-256 文件,不需要先发布;测试使用临时 HOME / CODEX_HOME,不操作真实手机和生产宿主配置。 + +新增公开入口仅为两种宿主各一个安装脚本,参数 --archive 用于本地候选验证,--help 展示说明。下载版本来自脚本所属版本,避免解析全仓 Latest 错装 DSH。没有发布资产就退出,不自动回落源码编译。新版本下载对应脚本,旧版本脚本用于回退。 + +验证:成功安装、坏校验、无发布、同名插件冲突、配置保留、重复安装、带空格目录、安装失败和回退;运行双方完整 check/package 和 WorkBuddy packed smoke。最脆弱前提是宿主原生安装接口和 macOS 下载执行权限;以实际临时配置安装核实前者,桌面首次运行和手机验收单独记录。需要公共 GitHub、nodejs.org、npm 网络,无新增账户/API key。不增加常驻服务。 + +涉及超过八个文件;源码和发布流程作为一个可审阅变更交付。回滚通过恢复对应宿主配置备份并重新安装旧版本实现,不删除用户数据、不修改 DSH、不自动强停手机任务。 + +## 实现验证命令 + +- `plugins/opengui`: `pnpm check`、`pnpm package`、`node scripts/test-installer.mjs`。 +- `workbuddy-plugin`: `npm run pack:release`、`npm run smoke:packed`、使用官方 Node 发行包执行 `node scripts/test-release-installer.mjs`(该测试需要发行包内的 npm)。 +- 安装 Skill:Skill Creator 的 `quick_validate.py skills/opengui-plugin-install`。 +- macOS 人工安装验收:使用临时 HOME / CODEX_HOME 和真实 Codex CLI 验证首装、重复安装;WorkBuddy 使用空 HOME 和不含 Node 的 PATH 验证私有运行时下载及三个配置入口。宿主桌面和真实手机验收仍沿用原发布清单。 diff --git a/plugins/opengui/README.md b/plugins/opengui/README.md index df37c71..fafa426 100644 --- a/plugins/opengui/README.md +++ b/plugins/opengui/README.md @@ -7,6 +7,36 @@ It includes a control Skill, local CLI/daemon, macOS ADB executable, and a read- device wall. It does not depend on, modify, install, update, or reload DSH. See [source provenance](SOURCE.md) and [privacy](docs/privacy.md). +## Install on macOS + +Once a release is published, download `opengui-codex--install.command` +and its `.sha256` from that same [Codex release](https://github.com/Core-Mate/OpenGUI/releases). +Verify the checksum in the download directory, then run `bash `. +The installer downloads the matching prebuilt package, checks it, prepares private Node, +and registers the standalone plugin using the native `codex plugin` commands. +No Git, pnpm, source build, or Xcode is required; Codex CLI with plugin support is required. +Start a **new chat**, choose OpenGUI, and ask to list connected phones without operating them. + +For agent-assisted installation, use the repository's +[installation Skill](../../skills/opengui-plugin-install/SKILL.md) and say +“Install OpenGUI for Codex”. The Skill resolves only complete releases for this host. +There is no public standalone download until the release gates below are satisfied. + +The installer uses the independent `opengui-standalone` marketplace and leaves the +repository's legacy marketplace unchanged. Finish OpenGUI tasks before upgrading. +A same-name plugin from another source is reported instead of silently replaced. +Packages and recovery inventories are retained under `~/.codex/opengui-codex/packages` +(or the selected `CODEX_HOME`). On failure, inspect the printed recovery directory; +source rollback is attempted through Codex commands, without resetting other settings. + +Maintainers can test an unpublished archive without installing build tools on the test Mac: + +```sh +bash scripts/install-macos.command --archive /absolute/path/opengui-codex-0.1.0.tar.gz +``` + +The adjacent `.sha256` file is required. Build and package once on the maintainer machine. + ## Development and verification Run commands from this directory: @@ -28,8 +58,8 @@ On macOS, `node scripts/smoke-archive.mjs ` verifies a packaged launcher using the checksum-pinned Node 22.23.2 archive in a temporary private cache. It does not connect to ADB or install into a Codex profile. -For local Codex installation, stage this package into a separate disposable -marketplace using Plugin Creator. Do not install both the legacy and standalone +For development-only manual staging, use a separate disposable marketplace with Plugin Creator. +The release installer above creates its own standalone source automatically. Do not install both the legacy and standalone `opengui` plugins into the same test task. Installing into your normal Codex profile or submitting to the public directory requires a separate user decision. diff --git a/plugins/opengui/README.zh-CN.md b/plugins/opengui/README.zh-CN.md index 6ed1ed9..29c0675 100644 --- a/plugins/opengui/README.zh-CN.md +++ b/plugins/opengui/README.zh-CN.md @@ -9,9 +9,35 @@ - 发送、发布、购买、删除需要对话确认和原生单次确认。 - 取消/关闭清理会话截图;空闲会话 30 分钟过期,空闲守护进程 5 分钟退出。 +## 普通用户安装 + +正式发布后,从对应 [Codex Release](https://github.com/Core-Mate/OpenGUI/releases) 下载 +`opengui-codex-版本-install.command` 及其 `.sha256`,在下载目录校验后运行: + +```sh +shasum -a 256 -c opengui-codex-0.1.0-install.command.sha256 +bash opengui-codex-0.1.0-install.command +``` + +安装器自动下载并校验预构建包、准备私有 Node、注册独立插件来源。需要带插件管理功能的 +Codex CLI,不需要 Git、pnpm、Xcode 或源码构建。完成后新开对话,选择 OpenGUI,先说 +“列出已连接手机,不操作手机”。USB 授权仍需在手机上批准。 + +也可让 Agent 使用仓库的 [安装 Skill](../../skills/opengui-plugin-install/SKILL.md), +说“帮我安装 OpenGUI Codex 插件”。它会自动查找匹配的正式版本并校验安装文件。 +当前尚未正式发布;没有完整 Release 时会明确停止,不会偷偷转为源码构建。 + +升级前结束旧任务。同名插件冲突会提示,不会自动移除。旧包和配置备份保存在 +`~/.codex/opengui-codex/packages`,使用 `CODEX_HOME` 时跟随该目录。回退可运行旧版本安装器。 + +维护者测试候选包:`bash scripts/install-macos.command --archive /绝对路径/opengui-codex-0.1.0.tar.gz`, +同目录需有归档的 `.sha256` 文件。 + +## 开发者构建 + 开发时在本目录运行 `pnpm install --frozen-lockfile --ignore-scripts`、 `pnpm check` 和 `pnpm package`。打包产物位于 `.artifacts/`。 -原仓库 marketplace 保持原样,测试安装必须使用独立临时来源。 +原仓库 marketplace 保持原样,安装器自动使用独立来源,不要求用户自行搭建 marketplace。 会话操作使用宿主提供的 `CODEX_THREAD_ID` 绑定当前任务,缺少该身份时拒绝执行。 会话列表仅返回当前任务的会话;设备墙令牌也按会话隔离。此机制防止任务间误操作, diff --git a/plugins/opengui/scripts/install-macos.command b/plugins/opengui/scripts/install-macos.command new file mode 100755 index 0000000..9a2e2a8 --- /dev/null +++ b/plugins/opengui/scripts/install-macos.command @@ -0,0 +1,156 @@ +#!/bin/bash +# Install a prebuilt release without a source checkout or system Node. +set -euo pipefail +umask 077 +HOST=codex +VERSION=0.1.0 +ARCHIVE_NAME=opengui-codex-$VERSION.tar.gz +usage() { + echo "OpenGUI for $HOST $VERSION (macOS arm64/x64)" + echo "Usage: bash $0 [--archive /absolute/path/$ARCHIVE_NAME]" + echo 'Downloads a verified prebuilt package and private Node. No sudo or source build.' + echo 'Finish existing OpenGUI tasks before upgrading. Keep old packages for rollback.' +} +archive= +case "${1:-}" in + --help|-h) usage; exit 0 ;; + --archive) [ "$#" = 2 ] || { usage; exit 1; }; archive=$2 ;; + '') [ "$#" = 0 ] || { usage; exit 1; } ;; + *) usage; exit 1 ;; +esac +[ "$(uname -s)" = Darwin ] || { echo 'Only macOS is supported.' >&2; exit 1; } +case "$(uname -m)" in + arm64) arch=arm64; node_sha=61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6 ;; + x86_64) arch=x64; node_sha=58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026 ;; + *) echo 'Unsupported architecture.' >&2; exit 1 ;; +esac +command -v codex >/dev/null || { echo 'Install the Codex CLI with plugin support first.' >&2; exit 1; } +codex plugin marketplace add --help >/dev/null +# Refuse redirected parent directories before creating installation state. +private_dir() { + local path=$1 cursor=$1 + while [ "$cursor" != / ]; do + [ ! -L "$cursor" ] || { echo "Refusing symlink: $cursor" >&2; exit 1; } + cursor=$(dirname "$cursor") + done + mkdir -p "$path" + [ "$(stat -f '%u' "$path")" = "$(id -u)" ] || { echo "Not owned by current user: $path" >&2; exit 1; } +} +root="${CODEX_HOME:-$HOME/.codex}/opengui-codex" +case "$root" in /*) ;; *) echo 'Installation home must be absolute.' >&2; exit 1 ;; esac +private_dir "$root" +lock="$root/installer.lock" +mkdir "$lock" 2>/dev/null || { echo "Installation busy or interrupted: inspect $lock before retrying." >&2; exit 1; } +temporary= +runtime_lock_owned=false +cleanup() { + [ -z "$temporary" ] || rm -rf "$temporary" + if [ "$runtime_lock_owned" = true ]; then rmdir "$root/runtime/install.lock"; fi + rmdir "$lock" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM +temporary=$(mktemp -d "$root/.install.XXXXXXXX") +fetch() { + curl --proto '=https' --proto-redir '=https' --tlsv1.2 --fail --location \ + --connect-timeout 15 --max-time 240 --retry 2 "$1" -o "$2" +} +if [ -z "$archive" ]; then + base="https://github.com/Core-Mate/OpenGUI/releases/download/opengui-$HOST-v$VERSION" + archive="$temporary/$ARCHIVE_NAME" + if ! fetch "$base/$ARCHIVE_NAME" "$archive" || ! fetch "$base/$ARCHIVE_NAME.sha256" "$archive.sha256"; then + echo "No downloadable $HOST $VERSION package, or network unavailable. Check the release page; installation stopped without changing host configuration." >&2 + exit 1 + fi +fi +[ -f "$archive" ] && [ -f "$archive.sha256" ] || { echo 'Archive and adjacent .sha256 file are required.' >&2; exit 1; } +# Parse the digest only. Never trust a sidecar filename as a local path. +expected=$(awk 'NR == 1 { print $1 }' "$archive.sha256") +[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || { echo 'Invalid SHA-256 sidecar.' >&2; exit 1; } +[ "$(shasum -a 256 "$archive" | awk '{print $1}')" = "$expected" ] || { echo 'Archive checksum mismatch; nothing installed.' >&2; exit 1; } +cp "$archive" "$temporary/verified.tar.gz" +# Recheck our private copy to avoid using a source archive changed during copy. +[ "$(shasum -a 256 "$temporary/verified.tar.gz" | awk '{print $1}')" = "$expected" ] || exit 1 +private_dir "$root/runtime" +node_name="node-v22.23.2-darwin-$arch" +node_dir="$root/runtime/$node_name" +node="$node_dir/bin/node" +valid_node() { + [ ! -L "$node_dir" ] && [ ! -L "$node_dir/bin" ] && [ ! -L "$node" ] && [ -x "$node" ] && [ -f "$node_dir/.verified" ] || return 1 + [ "$(sed -n '1p' "$node_dir/.verified")" = "$node_sha" ] && + [ "$(sed -n '2p' "$node_dir/.verified")" = "$(shasum -a 256 "$node" | awk '{print $1}')" ] +} +if ! valid_node; then + mkdir "$root/runtime/install.lock" 2>/dev/null || { echo "Runtime setup is busy; retry when the current setup finishes." >&2; exit 1; } + runtime_lock_owned=true + [ ! -e "$node_dir" ] && [ ! -L "$node_dir" ] || { echo "Invalid existing Node runtime: $node_dir. No running runtime was overwritten." >&2; exit 1; } + echo 'Preparing private Node.js 22.23.2 (~50 MB); no system installation.' + fetch "https://nodejs.org/dist/v22.23.2/$node_name.tar.gz" "$temporary/node.tar.gz" + [ "$(shasum -a 256 "$temporary/node.tar.gz" | awk '{print $1}')" = "$node_sha" ] || { echo 'Node checksum mismatch.' >&2; exit 1; } + tar -xzf "$temporary/node.tar.gz" -C "$temporary" + printf '%s\n%s\n' "$node_sha" "$(shasum -a 256 "$temporary/$node_name/bin/node" | awk '{print $1}')" > "$temporary/$node_name/.verified" + mv "$temporary/$node_name" "$node_dir" +fi +"$node" - "$root" "$temporary/verified.tar.gz" "$VERSION" <<'INSTALL_JS' +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); +const [root, archive, version] = process.argv.slice(2); +const run = (args) => execFileSync('codex', ['plugin', ...args], { encoding: 'utf8' }); +const listing = JSON.parse(run(['list', '--json'])); +if (!Array.isArray(listing.installed)) throw Error('Unsupported Codex plugin inventory; no configuration changed'); +const conflicts = listing.installed.filter(p => p.name === 'opengui' && p.marketplaceName !== 'opengui-standalone'); +if (conflicts.length) throw Error('OpenGUI already installed from another source: ' + conflicts.map(p => p.pluginId).join(', ') + '. Finish its tasks and remove that installation explicitly before retrying.'); +const entries = execFileSync('tar', ['-tzf', archive], { encoding: 'utf8' }).trim().split('\n'); +if (entries.some(p => !p.startsWith('opengui/') || p.split('/').includes('..'))) throw Error('Unsafe archive paths'); +const types = execFileSync('tar', ['-tvzf', archive], { encoding: 'utf8' }).trim().split('\n'); +if (types.some(p => !['-', 'd'].includes(p[0]))) throw Error('Archive links or special files rejected'); +const packages = path.join(root, 'packages'); +fs.mkdirSync(packages, { recursive: true }); +if (fs.lstatSync(packages).isSymbolicLink()) throw Error('Redirected packages directory'); +const install = fs.mkdtempSync(path.join(packages, version + '-')); +execFileSync('tar', ['-xzf', archive, '-C', install]); +const manifest = JSON.parse(fs.readFileSync(path.join(install, 'opengui/.codex-plugin/plugin.json'))); +if (manifest.name !== 'opengui' || manifest.version !== version) throw Error('Archive plugin/version mismatch'); +execFileSync(process.execPath, [path.join(install, 'opengui/lib/cli.js'), '--help'], { stdio: 'pipe' }); +const marketDir = path.join(install, '.agents/plugins'); +fs.mkdirSync(marketDir, { recursive: true }); +fs.writeFileSync(path.join(marketDir, 'marketplace.json'), JSON.stringify({ + name: 'opengui-standalone', interface: { displayName: 'OpenGUI for Codex' }, + plugins: [{ name: 'opengui', source: { source: 'local', path: './opengui' }, + policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' }, category: 'Developer Tools' }] +}, null, 2)); +const codexHome = process.env.CODEX_HOME || path.join(require('node:os').homedir(), '.codex'); +const config = path.join(codexHome, 'config.toml'); +if (fs.existsSync(config) && fs.lstatSync(config).isSymbolicLink()) throw Error('Redirected Codex configuration'); +if (fs.existsSync(config)) fs.copyFileSync(config, path.join(install, 'config.toml.before-install')); +fs.writeFileSync(path.join(install, 'previous-plugins.json'), JSON.stringify(listing, null, 2)); +console.log('Recovery files and immutable package: ' + install); +const markets = JSON.parse(run(['marketplace', 'list', '--json'])).marketplaces; +if (!Array.isArray(markets)) throw Error('Unsupported Codex marketplace inventory'); +const previous = markets.find(m => m.name === 'opengui-standalone'); +if (previous && (previous.marketplaceSource?.sourceType !== 'local' || !previous.root.startsWith(packages + path.sep))) throw Error('Existing marketplace is not owned by this installer; configuration retained'); +fs.writeFileSync(path.join(install, 'previous-marketplaces.json'), JSON.stringify(markets, null, 2)); +let removed = false, added = false; +try { + if (previous) { run(['marketplace', 'remove', 'opengui-standalone']); removed = true; } + console.log(run(['marketplace', 'add', install])); added = true; + console.log(run(['add', 'opengui@opengui-standalone'])); + const installed = JSON.parse(run(['list', '--json'])).installed; + if (!installed.some(p => p.name === 'opengui' && p.marketplaceName === 'opengui-standalone' && p.enabled && p.version === version)) throw Error('Codex did not report the expected enabled version'); +} catch (error) { + try { + if (added) run(['marketplace', 'remove', 'opengui-standalone']); + if (removed) { + run(['marketplace', 'add', previous.root]); + if (listing.installed.some(p => p.marketplaceName === 'opengui-standalone' && p.enabled)) run(['add', 'opengui@opengui-standalone']); + } + } catch (recovery) { console.error('Automatic source recovery failed: ' + recovery.message); } + console.error('Installation failed. Inspect the saved inventories and configuration backup in ' + install); + throw error; +} +console.log('Installed. Start a NEW Codex chat, choose OpenGUI, and ask: list connected phones without operating them.'); +console.log('Rollback: finish OpenGUI tasks, then reinstall the previous version using its installer; previous packages and configuration backup are retained.'); + +INSTALL_JS diff --git a/plugins/opengui/scripts/opengui b/plugins/opengui/scripts/opengui index 646ecc7..7f8817c 100755 --- a/plugins/opengui/scripts/opengui +++ b/plugins/opengui/scripts/opengui @@ -26,7 +26,7 @@ case "$(uname -m)" in *) printf '%s\n' 'Unsupported macOS architecture.' >&2; exit 1 ;; esac runtime_version=22.23.2 -data_root=${OPENGUI_CODEX_DATA_DIR:-"$HOME/.codex/opengui-codex"} +data_root=${OPENGUI_CODEX_DATA_DIR:-"${CODEX_HOME:-$HOME/.codex}/opengui-codex"} case "$data_root" in /*) ;; *) printf '%s\n' 'OPENGUI_CODEX_DATA_DIR must be an absolute path.' >&2; exit 1 ;; diff --git a/plugins/opengui/scripts/package.mjs b/plugins/opengui/scripts/package.mjs index fbcc170..f505f5b 100644 --- a/plugins/opengui/scripts/package.mjs +++ b/plugins/opengui/scripts/package.mjs @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto' -import { mkdtemp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises' +import { copyFile, mkdtemp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { execFileSync } from 'node:child_process' @@ -20,7 +20,9 @@ try { execFileSync('zip', ['-q', '-r', join(temp, 'package.zip'), 'opengui'], { cwd: temp, stdio: 'inherit' }) await rename(join(temp, 'package.tar.gz'), archive) await rename(join(temp, 'package.zip'), upload) - for (const path of [archive, upload]) { + const installer = join(output, 'opengui-codex-' + pkg.version + '-install.command') + await copyFile(join(root, 'scripts/install-macos.command'), installer) + for (const path of [archive, upload, installer]) { const checksum = createHash('sha256').update(await readFile(path)).digest('hex') await writeFile(path + '.sha256', checksum + ' ' + path.slice(path.lastIndexOf('/') + 1) + '\n') console.log(path) diff --git a/plugins/opengui/scripts/test-installer.mjs b/plugins/opengui/scripts/test-installer.mjs new file mode 100644 index 0000000..46b94d2 --- /dev/null +++ b/plugins/opengui/scripts/test-installer.mjs @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { execFileSync, spawnSync } from 'node:child_process' +import { cp, copyFile, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +if (process.platform !== 'darwin') { console.log('Installer execution requires macOS; shell syntax is checked on every host.'); process.exit(0) } +const root = fileURLToPath(new URL('..', import.meta.url)) +const temporary = await realpath(await mkdtemp(join(tmpdir(), 'opengui-codex-installer-'))) +try { + const home = join(temporary, 'home with spaces') + const codexHome = join(home, '.codex') + const bin = join(home, 'bin') + await mkdir(bin, { recursive: true }) + const fixture = join(bin, 'codex') + await writeFile(fixture, `#!${process.execPath} +const fs = require('fs'), p = require('path'); +const state = p.join(process.env.HOME, 'plugins.json'); +const args = process.argv.slice(2); +if (args.includes('--help')) process.exit(0); +if (args[1] === 'list') console.log(fs.existsSync(state) ? fs.readFileSync(state, 'utf8') : '{"installed":[]}'); +else if (args[1] === 'marketplace') { + const file = p.join(process.env.HOME, 'market'); + if (args[2] === 'list') { const root = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null; console.log(JSON.stringify({marketplaces:root?[{name:'opengui-standalone',root,marketplaceSource:{sourceType:'local',source:root}}]:[]})); } + else if (args[2] === 'remove') fs.rmSync(file, {force:true}); + else if (fs.existsSync(file)) throw Error('Duplicate marketplace source'); + else fs.writeFileSync(file, args[3]); +} +else if (args[1] === 'add') { + if (process.env.FAIL_INSTALL) process.exit(7); + const manifest = JSON.parse(fs.readFileSync(p.join(fs.readFileSync(p.join(process.env.HOME, 'market'), 'utf8'), 'opengui/.codex-plugin/plugin.json'))); + fs.writeFileSync(state, JSON.stringify({installed:[{name:'opengui',marketplaceName:'opengui-standalone',pluginId:'opengui@opengui-standalone',version:manifest.version,enabled:true}]})); +} +`, { mode: 0o755 }) + const runtime = join(codexHome, 'opengui-codex/runtime', `node-v22.23.2-darwin-${process.arch}`) + await mkdir(join(runtime, 'bin'), { recursive: true }) + await writeFile(join(runtime, 'bin/node'), '#!/bin/sh\nexec ' + JSON.stringify(process.execPath) + ' \"$@\"\n', { mode: 0o755 }) + const digest = createHash('sha256').update(await readFile(join(runtime, 'bin/node'))).digest('hex') + const archiveSha = process.arch === 'arm64' ? '61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6' : '58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026' + await writeFile(join(runtime, '.verified'), archiveSha + '\n' + digest + '\n') + await writeFile(join(codexHome, 'config.toml'), '# Existing unrelated settings\n') + const env = { ...process.env, HOME: home, CODEX_HOME: codexHome, PATH: bin + ':' + process.env.PATH } + const archive = join(root, '.artifacts/opengui-codex-0.1.0.tar.gz') + const script = join(root, 'scripts/install-macos.command') + const run = (file = archive, extra = {}) => spawnSync('bash', [script, '--archive', file], { env: { ...env, ...extra }, encoding: 'utf8' }) + let result = run(); assert.equal(result.status, 0, result.stderr) + assert.match(result.stdout, /Installed. Start a NEW/) + result = run(); assert.equal(result.status, 0, result.stderr) + assert.equal(await readFile(join(codexHome, 'config.toml'), 'utf8'), '# Existing unrelated settings\n') + await writeFile(join(bin, 'curl'), '#!/bin/sh\nexit 22\n', { mode: 0o755 }) + result = spawnSync('bash', [script], { env, encoding: 'utf8' }) + assert.notEqual(result.status, 0); assert.match(result.stderr, /No downloadable codex/) + const bad = join(temporary, 'bad.tar.gz'); await cp(archive, bad); await writeFile(bad + '.sha256', '0'.repeat(64)) + result = run(bad); assert.notEqual(result.status, 0); assert.match(result.stderr, /checksum mismatch/) + result = run(archive, { FAIL_INSTALL: '1' }); assert.notEqual(result.status, 0) + assert.equal(await readFile(join(codexHome, 'config.toml'), 'utf8'), '# Existing unrelated settings\n') + await writeFile(join(home, 'plugins.json'), JSON.stringify({ installed: [{ name: 'opengui', marketplaceName: 'personal', pluginId: 'opengui@personal' }] })) + result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /another source/) + console.log('PASS: packaged install, repeat install, spaces, checksum rejection, host failure and duplicate-source rejection; existing configuration retained.') +} finally { await rm(temporary, { recursive: true, force: true }) } diff --git a/plugins/opengui/scripts/validate.mjs b/plugins/opengui/scripts/validate.mjs index ead4897..31afaf6 100644 --- a/plugins/opengui/scripts/validate.mjs +++ b/plugins/opengui/scripts/validate.mjs @@ -10,6 +10,8 @@ import { stagePlugin } from './stage.mjs' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const json = async path => JSON.parse(await readFile(join(root, path), 'utf8')) const pkg = await json('package.json') +assert((await readFile(join(root, 'scripts/install-macos.command'), 'utf8')).includes('VERSION=' + pkg.version + '\n'), 'Release installer version mismatch') +execFileSync('/bin/bash', ['-n', join(root, 'scripts/install-macos.command')]) const plugin = await json('.codex-plugin/plugin.json') assert.equal(plugin.name, 'opengui') assert.match(pkg.version, /^\d+\.\d+\.\d+$/) diff --git a/skills/opengui-plugin-install/SKILL.md b/skills/opengui-plugin-install/SKILL.md new file mode 100644 index 0000000..fe6c078 --- /dev/null +++ b/skills/opengui-plugin-install/SKILL.md @@ -0,0 +1,22 @@ +--- +name: opengui-plugin-install +description: Install or upgrade the standalone OpenGUI Codex or WorkBuddy plugin on macOS from verified prebuilt GitHub releases. Use for plugin installation, not phone actions or DSH installation. +--- + +# OpenGUI plugin installation + +Infer Codex or WorkBuddy from the request; ask only if the target is missing. The two plugins have independent versions and installation directories. Do not install both unless requested. + +1. Read the public GitHub releases API for `Core-Mate/OpenGUI`, following pagination. Select the newest non-draft, non-prerelease semantic version with tag `opengui-codex-vX.Y.Z` or `opengui-workbuddy-vX.Y.Z` and all matching assets below. An explicit requested version takes precedence; never silently substitute a different version. Do not use the repository-wide latest release, which may be DSH or an APK. +2. Download `opengui-HOST-X.Y.Z-install.command` and its `.sha256` asset from that release into a fresh temporary directory. Use HTTPS with redirect-to-HTTPS only. Require exact filenames and verify SHA-256 before executing the installer. The checksum establishes integrity relative to the selected public release, not an independent publisher signature. +3. Finish existing OpenGUI tasks before an upgrade. WorkBuddy must be closed before its configuration can be changed; do not kill it or its phone/mirror processes. Codex requires the native CLI with `codex plugin` support. Never remove a conflicting plugin source without the user's authorization. +4. Run `bash /absolute/path/opengui-HOST-X.Y.Z-install.command`. It downloads and verifies the matching package, prepares private Node 22.23.2, installs into a fresh version directory, and configures only the selected host. No Git clone, pnpm, source compilation, or user-run test suite is required. +5. Read the result. On success, ask for a new Codex chat or a WorkBuddy restart, then verify read-only device discovery. USB and system permissions remain user actions. Installation success does not prove phone control, desktop visibility, or two-device acceptance. + +Required release assets: +- Codex: `opengui-codex-X.Y.Z.tar.gz`, its `.sha256`, and the installer plus its `.sha256`. +- WorkBuddy: `opengui-mcp-X.Y.Z.tgz`, its `.sha256`, and the installer plus its `.sha256`. + +If no complete release exists or downloads fail, report that precise state. Do not replace the installer with source builds or invent a working download link. For explicitly requested candidate testing, use a maintainer-provided archive with its adjacent checksum and the matching source installer: `bash install-macos.command --archive /absolute/package.tar.gz` (WorkBuddy uses `.tgz`). Keep candidate and published status separate. + +Rollback uses the previous version's verified installer after tasks end. WorkBuddy also records scoped configuration backups in `~/.workbuddy/opengui/local-install.json`; Codex retains previous inventories and a configuration backup beside each immutable package. Preserve subsequent unrelated edits when recovering; do not reset an entire host or touch DSH. diff --git a/workbuddy-plugin/README.md b/workbuddy-plugin/README.md index 6f7bbe7..69fc0a6 100644 --- a/workbuddy-plugin/README.md +++ b/workbuddy-plugin/README.md @@ -23,7 +23,7 @@ There is no DSH/Codex dependency, installer, UI injection, browser agent, custom - Node `^22.19.0 || >=24`, declared in the connector for WorkBuddy's managed runtime. - Bundled ADB: macOS arm64/x64, Linux x64, Windows x64. Other architectures need an explicit compatible `OPENGUI_ADB_PATH`; Unicode support is limited to the pinned scrcpy platforms. - Android USB debugging and user-approved authorization. The connector never accepts that authorization automatically. -- First installation needs GitHub and npm access. Unicode input downloads a checksum-pinned official scrcpy archive on first use. A cached installation may be restarted offline after all required dependencies and scrcpy assets are cached. +- First installation needs GitHub, nodejs.org and npm access. Unicode input downloads a checksum-pinned official scrcpy archive on first use. A cached installation may be restarted offline after all required dependencies and scrcpy assets are cached. Screenshots and visible phone data are returned to the current WorkBuddy model. They are not written to disk by this runtime, although WorkBuddy may retain tool results. Device-wall URLs contain private viewing capabilities; do not share them. HTTP serves only `127.0.0.1`, checks Host/Origin and per-session tokens, sends no-store headers, and loads no remote assets. The wall stops reading frames after session termination. @@ -33,34 +33,26 @@ Do not run DSH, Codex, manual ADB, or another automation host against the same p ## Install on macOS -This is the source-install path for the unpublished `0.2.0` candidate. Use macOS with WorkBuddy 5.5.3 as the acceptance baseline, a vision-and-tool-capable model, Git, Node.js 22.19+ within 22.x or 24+, and npm. Building the native helpers requires Xcode command-line tools (`xcode-select -p` checks availability). ADB is bundled and scrcpy is downloaded and verified automatically; no Homebrew installation is needed. Prebuilt-package users do not need Xcode, but there is no published candidate download yet. - -Clone the candidate into a new directory, without replacing an existing checkout: - -```sh -git clone --branch codex/workbuddy-vlm-persistent-mirror --single-branch \ - https://github.com/Core-Mate/OpenGUI.git opengui-workbuddy-candidate -cd opengui-workbuddy-candidate/workbuddy-plugin -npm ci -npm run pack:release -npm run smoke:packed -``` - -If an old WorkBuddy OpenGUI installation is running, finish or cancel its tasks and explicitly close its mirrors before upgrading. Quit WorkBuddy before switching configuration. Do not kill unrelated scrcpy/ADB processes. Run the following in the same terminal and source directory, stopping if any command fails: - -```sh -OPENGUI_ARCHIVE="$PWD/dist/opengui-mcp-0.2.0.tgz" -(cd dist && shasum -a 256 -c opengui-mcp-0.2.0.tgz.sha256) -OPENGUI_NODE="$(node -p 'process.execPath')" -mkdir -p "$HOME/.workbuddy/opengui/packages" -OPENGUI_INSTALL="$(mktemp -d "$HOME/.workbuddy/opengui/packages/0.2.0-local.XXXXXX")" -npm install --prefix "$OPENGUI_INSTALL" --no-audit --no-fund "$OPENGUI_ARCHIVE" -node scripts/install-local.mjs \ - --package-dir "$OPENGUI_INSTALL/node_modules/opengui-mcp" \ - --node "$OPENGUI_NODE" -``` - -Keep that Node executable available: WorkBuddy uses its absolute path for MCP and Hooks. The installer runs from the built source checkout, not from inside the tarball. It records backups and incrementally updates `~/.workbuddy/mcp.json`, `~/.workbuddy/settings.json`, and `~/.workbuddy/skills/opengui/SKILL.md`. Each installation gets a new package directory; keep the old one for rollback. Do not use the connector ZIP's Release URL until the matching Release asset has been published. +The host-specific release now includes `opengui-workbuddy--install.command` +and its SHA-256 sidecar. After publication, download and verify the installer, quit +WorkBuddy after finishing phone tasks, and run it with `bash`. It fetches the matching +verified package, prepares private Node, installs with lifecycle scripts disabled, +and invokes the configuration installer shipped inside the package. No source checkout, +system Node, Xcode, or user-run tests are required. Existing MCP servers, Hooks, +configuration backups and old version directories are preserved. Reopen WorkBuddy +and trust the MCP before read-only device discovery. + +For unpublished candidates use `bash scripts/install-macos.command --archive /absolute/opengui-mcp-0.2.0.tgz` +with the adjacent `.sha256` file. This does not bypass public release acceptance. +See the [Chinese installation guide](README.zh-CN.md#macos-安装) and the +[agent installation Skill](../skills/opengui-plugin-install/SKILL.md). + +### Maintainer candidate builds + +Build once with `npm ci`, `npm run pack:release`, and `npm run smoke:packed` +in this directory. This build machine needs Node/npm and Xcode command-line tools. +Transfer the tarball, sidecar and installer from `dist/` to the test Mac; the test +Mac needs no build tools. The configuration installer is included inside the tarball. ### Verify the installation diff --git a/workbuddy-plugin/README.zh-CN.md b/workbuddy-plugin/README.zh-CN.md index 332b96f..661ad88 100644 --- a/workbuddy-plugin/README.zh-CN.md +++ b/workbuddy-plugin/README.zh-CN.md @@ -16,34 +16,41 @@ DSH、Codex 的源码、依赖、安装配置、缓存和发布流程均不复 ## macOS 安装 -当前提供未发布候选版的源码安装路径,以 macOS、WorkBuddy 5.5.3 为验收基线。需要支持工具调用和图片的模型,以及 Git、Node.js 22.19 及以上的 22.x 或 24 及以上版本、npm。源码构建还需要 Xcode 命令行工具,可用 `xcode-select -p` 检查。ADB 已随包提供,scrcpy 会自动下载并校验,不需要通过 Homebrew 安装。使用预构建包不需要 Xcode,但当前尚无正式发布的候选包下载入口。 +当前仍为未发布候选版。正式发布后,普通用户从对应 [WorkBuddy Release](https://github.com/Core-Mate/OpenGUI/releases) +下载 `opengui-workbuddy-版本-install.command` 和它的 `.sha256`。结束旧 OpenGUI 任务、关闭投屏并退出 WorkBuddy 后,在下载目录运行: -克隆候选分支到新目录,不覆盖已有工作区: +```sh +shasum -a 256 -c opengui-workbuddy-0.2.0-install.command.sha256 +bash opengui-workbuddy-0.2.0-install.command +``` + +安装器自动下载并校验预构建包、准备私有 Node 22.23.2、安装依赖,并备份及增量配置 MCP、Skill 和生命周期 Hooks。 +不需要 Git、系统 Node、pnpm、Xcode 或编译源码。需要访问 GitHub、nodejs.org 和 npm;ADB 随包提供,scrcpy 首次使用自动下载。 +其他 MCP 和 Hooks 保留,旧版本包不覆盖。安装完重开 WorkBuddy,按提示启用和信任 OpenGUI MCP。 + +也可让 Agent 使用 [安装 Skill](../skills/opengui-plugin-install/SKILL.md),说“帮我安装 OpenGUI WorkBuddy 插件”。 +没有完整发布资产时会停止并说明原因,不会改走源码构建。WorkBuddy 5.5.3、macOS 和支持图片与工具的模型仍是验收基线。 + +### 开发者构建与候选测试 + +以下只在维护者构建机器执行,需要 Node.js 22.19+ 的 22.x 或 24+、npm 和 Xcode 命令行工具: ```sh -git clone --branch codex/workbuddy-vlm-persistent-mirror --single-branch \ - https://github.com/Core-Mate/OpenGUI.git opengui-workbuddy-candidate -cd opengui-workbuddy-candidate/workbuddy-plugin +cd workbuddy-plugin npm ci npm run pack:release npm run smoke:packed ``` -升级前先完成或取消旧 WorkBuddy OpenGUI 任务,明确关闭旧投屏窗口,再退出 WorkBuddy。不要批量终止 scrcpy 或 ADB 进程。以下命令在同一个终端、上述源码目录执行,任一步失败就停止: +把 `dist/opengui-mcp-0.2.0.tgz`、其 `.sha256` 和 `dist/opengui-workbuddy-0.2.0-install.command` +送到测试 Mac,退出 WorkBuddy 后运行: ```sh -OPENGUI_ARCHIVE="$PWD/dist/opengui-mcp-0.2.0.tgz" -(cd dist && shasum -a 256 -c opengui-mcp-0.2.0.tgz.sha256) -OPENGUI_NODE="$(node -p 'process.execPath')" -mkdir -p "$HOME/.workbuddy/opengui/packages" -OPENGUI_INSTALL="$(mktemp -d "$HOME/.workbuddy/opengui/packages/0.2.0-local.XXXXXX")" -npm install --prefix "$OPENGUI_INSTALL" --no-audit --no-fund "$OPENGUI_ARCHIVE" -node scripts/install-local.mjs \ - --package-dir "$OPENGUI_INSTALL/node_modules/opengui-mcp" \ - --node "$OPENGUI_NODE" +bash opengui-workbuddy-0.2.0-install.command --archive /绝对路径/opengui-mcp-0.2.0.tgz ``` -保留这个 Node 可执行文件,MCP 和 Hooks 使用它的绝对路径。安装脚本从已构建的源码目录运行,不在 tarball 内。安装器先备份,再增量更新 `~/.workbuddy/mcp.json`、`~/.workbuddy/settings.json` 和 `~/.workbuddy/skills/opengui/SKILL.md`,保留其他插件和 Hooks,拒绝软链接重定向。每次安装使用新目录,旧包保留用于回退。正式 Release 资源发布前,不要直接使用连接器 ZIP 中的 Release 下载地址。 +无需把源码、编译器或测试工具带到测试机器。底层 `scripts/install-local.mjs` 已随包提供; +安装器统一调用它,用户不必手动建立目录、填写 Node 路径或逐项安装 Hooks。 ### 安装验证与排查 diff --git a/workbuddy-plugin/package.json b/workbuddy-plugin/package.json index a5aaebe..27ea016 100644 --- a/workbuddy-plugin/package.json +++ b/workbuddy-plugin/package.json @@ -6,7 +6,7 @@ "license": "SEE LICENSE IN LICENSE", "repository": { "type": "git", "url": "https://github.com/Core-Mate/OpenGUI.git", "directory": "workbuddy-plugin" }, "bin": { "opengui-mcp": "lib/mcp.js" }, - "files": ["lib", "assets", "LICENSE", "NOTICE.md", "README.md"], + "files": ["scripts/install-local.mjs", "lib", "assets", "LICENSE", "NOTICE.md", "README.md"], "engines": { "node": "^22.19.0 || >=24" }, "scripts": { "build": "tsc -p tsconfig.json && node scripts/finalize.mjs", diff --git a/workbuddy-plugin/scripts/install-macos.command b/workbuddy-plugin/scripts/install-macos.command new file mode 100755 index 0000000..a2345f7 --- /dev/null +++ b/workbuddy-plugin/scripts/install-macos.command @@ -0,0 +1,114 @@ +#!/bin/bash +# Install a prebuilt release without a source checkout or system Node. +set -euo pipefail +umask 077 +HOST=workbuddy +VERSION=0.2.0 +ARCHIVE_NAME=opengui-mcp-$VERSION.tgz +usage() { + echo "OpenGUI for $HOST $VERSION (macOS arm64/x64)" + echo "Usage: bash $0 [--archive /absolute/path/$ARCHIVE_NAME]" + echo 'Downloads a verified prebuilt package and private Node. No sudo or source build.' + echo 'Finish existing OpenGUI tasks before upgrading. Keep old packages for rollback.' +} +archive= +case "${1:-}" in + --help|-h) usage; exit 0 ;; + --archive) [ "$#" = 2 ] || { usage; exit 1; }; archive=$2 ;; + '') [ "$#" = 0 ] || { usage; exit 1; } ;; + *) usage; exit 1 ;; +esac +[ "$(uname -s)" = Darwin ] || { echo 'Only macOS is supported.' >&2; exit 1; } +case "$(uname -m)" in + arm64) arch=arm64; node_sha=61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6 ;; + x86_64) arch=x64; node_sha=58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026 ;; + *) echo 'Unsupported architecture.' >&2; exit 1 ;; +esac +if pgrep -x WorkBuddy >/dev/null; then echo 'Quit WorkBuddy after finishing OpenGUI tasks and closing its mirrors, then rerun this installer.' >&2; exit 1; fi +# Refuse redirected parent directories before creating installation state. +private_dir() { + local path=$1 cursor=$1 + while [ "$cursor" != / ]; do + [ ! -L "$cursor" ] || { echo "Refusing symlink: $cursor" >&2; exit 1; } + cursor=$(dirname "$cursor") + done + mkdir -p "$path" + [ "$(stat -f '%u' "$path")" = "$(id -u)" ] || { echo "Not owned by current user: $path" >&2; exit 1; } +} +root="$HOME/.workbuddy/opengui" +case "$root" in /*) ;; *) echo 'Installation home must be absolute.' >&2; exit 1 ;; esac +private_dir "$root" +lock="$root/installer.lock" +mkdir "$lock" 2>/dev/null || { echo "Installation busy or interrupted: inspect $lock before retrying." >&2; exit 1; } +temporary= +runtime_lock_owned=false +cleanup() { + [ -z "$temporary" ] || rm -rf "$temporary" + if [ "$runtime_lock_owned" = true ]; then rmdir "$root/runtime/install.lock"; fi + rmdir "$lock" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM +temporary=$(mktemp -d "$root/.install.XXXXXXXX") +fetch() { + curl --proto '=https' --proto-redir '=https' --tlsv1.2 --fail --location \ + --connect-timeout 15 --max-time 240 --retry 2 "$1" -o "$2" +} +if [ -z "$archive" ]; then + base="https://github.com/Core-Mate/OpenGUI/releases/download/opengui-$HOST-v$VERSION" + archive="$temporary/$ARCHIVE_NAME" + if ! fetch "$base/$ARCHIVE_NAME" "$archive" || ! fetch "$base/$ARCHIVE_NAME.sha256" "$archive.sha256"; then + echo "No downloadable $HOST $VERSION package, or network unavailable. Check the release page; installation stopped without changing host configuration." >&2 + exit 1 + fi +fi +[ -f "$archive" ] && [ -f "$archive.sha256" ] || { echo 'Archive and adjacent .sha256 file are required.' >&2; exit 1; } +# Parse the digest only. Never trust a sidecar filename as a local path. +expected=$(awk 'NR == 1 { print $1 }' "$archive.sha256") +[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || { echo 'Invalid SHA-256 sidecar.' >&2; exit 1; } +[ "$(shasum -a 256 "$archive" | awk '{print $1}')" = "$expected" ] || { echo 'Archive checksum mismatch; nothing installed.' >&2; exit 1; } +cp "$archive" "$temporary/verified.tar.gz" +# Recheck our private copy to avoid using a source archive changed during copy. +[ "$(shasum -a 256 "$temporary/verified.tar.gz" | awk '{print $1}')" = "$expected" ] || exit 1 +private_dir "$root/runtime" +node_name="node-v22.23.2-darwin-$arch" +node_dir="$root/runtime/$node_name" +node="$node_dir/bin/node" +valid_node() { + [ ! -L "$node_dir" ] && [ ! -L "$node_dir/bin" ] && [ ! -L "$node" ] && [ -x "$node" ] && [ -f "$node_dir/.verified" ] || return 1 + [ "$(sed -n '1p' "$node_dir/.verified")" = "$node_sha" ] && + [ "$(sed -n '2p' "$node_dir/.verified")" = "$(shasum -a 256 "$node" | awk '{print $1}')" ] +} +if ! valid_node; then + mkdir "$root/runtime/install.lock" 2>/dev/null || { echo "Runtime setup is busy; retry when the current setup finishes." >&2; exit 1; } + runtime_lock_owned=true + [ ! -e "$node_dir" ] && [ ! -L "$node_dir" ] || { echo "Invalid existing Node runtime: $node_dir. No running runtime was overwritten." >&2; exit 1; } + echo 'Preparing private Node.js 22.23.2 (~50 MB); no system installation.' + fetch "https://nodejs.org/dist/v22.23.2/$node_name.tar.gz" "$temporary/node.tar.gz" + [ "$(shasum -a 256 "$temporary/node.tar.gz" | awk '{print $1}')" = "$node_sha" ] || { echo 'Node checksum mismatch.' >&2; exit 1; } + tar -xzf "$temporary/node.tar.gz" -C "$temporary" + printf '%s\n%s\n' "$node_sha" "$(shasum -a 256 "$temporary/$node_name/bin/node" | awk '{print $1}')" > "$temporary/$node_name/.verified" + mv "$temporary/$node_name" "$node_dir" +fi +"$node" - "$root" "$temporary/verified.tar.gz" "$VERSION" <<'INSTALL_JS' +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); +const [root, archive, version] = process.argv.slice(2); +const packages = path.join(root, 'packages'); +fs.mkdirSync(packages, { recursive: true }); +if (fs.lstatSync(packages).isSymbolicLink()) throw Error('Redirected packages directory'); +const install = fs.mkdtempSync(path.join(packages, version + '-')); +const npm = path.resolve(process.execPath, '../../lib/node_modules/npm/bin/npm-cli.js'); +execFileSync(process.execPath, [npm, 'install', '--prefix', install, '--ignore-scripts', '--no-audit', '--no-fund', archive], { stdio: 'inherit' }); +const pkg = path.join(install, 'node_modules/opengui-mcp'); +const meta = JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'))); +if (meta.name !== 'opengui-mcp' || meta.version !== version) throw Error('Archive package/version mismatch'); +// Verify native dependencies before switching the host configuration. +execFileSync(process.execPath, ['--input-type=module', '-e', 'await import("sharp"); await import("@modelcontextprotocol/sdk/client/index.js")'], { cwd: pkg, stdio: 'inherit' }); +execFileSync(process.execPath, [path.join(pkg, 'scripts/install-local.mjs'), '--package-dir', pkg, '--node', process.execPath], { stdio: 'inherit' }); +console.log('Installed MCP, Skill and lifecycle Hooks. Reopen WorkBuddy, trust OpenGUI MCP, choose /opengui and ask to list phones without operating them.'); +console.log('Rollback backups: ' + path.join(root, 'local-install.json') + '. Old packages are retained.'); + +INSTALL_JS diff --git a/workbuddy-plugin/scripts/package.mjs b/workbuddy-plugin/scripts/package.mjs index fad1d86..f9bed3f 100644 --- a/workbuddy-plugin/scripts/package.mjs +++ b/workbuddy-plugin/scripts/package.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import { execFileSync } from 'node:child_process' -import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises' +import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { join } from 'node:path' import { zipSync } from 'fflate' @@ -15,7 +15,7 @@ assert(npmCli, 'Run through npm run pack:release') const [packed] = JSON.parse(execFileSync(process.execPath, [npmCli, 'pack', '--json', '--ignore-scripts', '--pack-destination', destination], { cwd: root, encoding: 'utf8' })) assert.equal(packed.filename, `opengui-mcp-${version}.tgz`) const files = packed.files.map(file => file.path) -for (const expected of ['lib/mcp.js', 'lib/broker-main.js', 'lib/host-hook.js', 'lib/automation.js', 'lib/installation.js', 'lib/opengui-SKILL.md', 'assets/platform-tools/darwin/adb', 'assets/platform-tools/linux-x64/adb', 'assets/platform-tools/win32-x64/adb.exe', 'LICENSE', 'NOTICE.md']) assert(files.includes(expected), expected) +for (const expected of ['scripts/install-local.mjs', 'lib/mcp.js', 'lib/broker-main.js', 'lib/host-hook.js', 'lib/automation.js', 'lib/installation.js', 'lib/opengui-SKILL.md', 'assets/platform-tools/darwin/adb', 'assets/platform-tools/linux-x64/adb', 'assets/platform-tools/win32-x64/adb.exe', 'LICENSE', 'NOTICE.md']) assert(files.includes(expected), expected) assert(!files.some(path => /confirmation|__pycache__|\.pyc$|\.DS_Store$/.test(path)), 'Obsolete approval code or build noise in package') assert(!files.some(path => /(^|\/)(src|tests|node_modules|\.env|connector)(\/|$)|codex|dsh/i.test(path)), 'Unexpected package contents') for (const path of ['lib/mcp.js', 'assets/platform-tools/darwin/adb', 'assets/platform-tools/linux-x64/adb']) { @@ -32,7 +32,9 @@ async function collect(relative = '') { await collect() const connector = `opengui-workbuddy-connector-${version}.zip` await writeFile(join(destination, connector), zipSync(entries, { level: 9 })) -for (const name of [packed.filename, connector]) { +const installer = `opengui-workbuddy-${version}-install.command` +await copyFile(join(root, 'scripts/install-macos.command'), join(destination, installer)) +for (const name of [packed.filename, connector, installer]) { const hash = createHash('sha256').update(await readFile(join(destination, name))).digest('hex') await writeFile(join(destination, `${name}.sha256`), `${hash} ${name}\n`) console.log(`${name} ${hash}`) diff --git a/workbuddy-plugin/scripts/publish.mjs b/workbuddy-plugin/scripts/publish.mjs index 85603db..1aaf620 100644 --- a/workbuddy-plugin/scripts/publish.mjs +++ b/workbuddy-plugin/scripts/publish.mjs @@ -10,7 +10,7 @@ const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8' const tag = `opengui-workbuddy-v${version}` assert.equal(process.env.GITHUB_REF_NAME, tag, 'Only publish the exact independent WorkBuddy version tag') execFileSync(process.execPath, [join(root, 'scripts/validate.mjs'), '--release'], { cwd: root, stdio: 'inherit' }) -const assets = [`opengui-mcp-${version}.tgz`, `opengui-workbuddy-connector-${version}.zip`].flatMap(name => [name, `${name}.sha256`]) +const assets = [`opengui-mcp-${version}.tgz`, `opengui-workbuddy-connector-${version}.zip`, `opengui-workbuddy-${version}-install.command`].flatMap(name => [name, `${name}.sha256`]) const gh = args => execFileSync('gh', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) let exists = false try { gh(['release', 'view', tag, '--json', 'tagName']); exists = true } catch (error) { diff --git a/workbuddy-plugin/scripts/test-release-installer.mjs b/workbuddy-plugin/scripts/test-release-installer.mjs new file mode 100644 index 0000000..64595f4 --- /dev/null +++ b/workbuddy-plugin/scripts/test-release-installer.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { spawnSync } from 'node:child_process' +import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +if (process.platform !== 'darwin') { console.log('Release installer execution requires macOS.'); process.exit(0) } +const root = fileURLToPath(new URL('..', import.meta.url)) +const temporary = await realpath(await mkdtemp(join(tmpdir(), 'opengui-workbuddy-installer-'))) +try { + const home = join(temporary, 'home with spaces'), config = join(home, '.workbuddy'), bin = join(home, 'bin') + await mkdir(bin, { recursive: true }) + // Only the isolated test host is considered stopped; never quit the real app. + await writeFile(join(bin, 'pgrep'), '#!/bin/sh\nexit "${TEST_HOST_RUNNING:-1}"\n', {mode:0o755}) + const runtime = join(config, 'opengui/runtime', `node-v22.23.2-darwin-${process.arch}`) + await mkdir(join(runtime, 'bin'), { recursive: true }) + await writeFile(join(runtime, 'bin/node'), '#!/bin/sh\nexec ' + JSON.stringify(process.execPath) + ' "$@"\n', { mode:0o755 }) + const digest = createHash('sha256').update(await readFile(join(runtime, 'bin/node'))).digest('hex') + const sha = process.arch === 'arm64' ? '61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6' : '58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026' + await writeFile(join(runtime, '.verified'), sha + '\n' + digest + '\n') + await writeFile(join(config, 'mcp.json'), JSON.stringify({mcpServers:{other:{command:'keep-me'}}})) + await writeFile(join(config, 'settings.json'), JSON.stringify({custom:true,hooks:{Stop:[{hooks:[{type:'command',command:'other-hook'}]}]}})) + const archive = join(root, 'dist/opengui-mcp-0.2.0.tgz') + const run = (extra={}) => spawnSync('bash', [join(root, 'scripts/install-macos.command'), '--archive', archive], {encoding:'utf8',env:{...process.env,HOME:home,PATH:bin+':'+process.env.PATH,...extra}}) + let result=run({TEST_HOST_RUNNING:'0'}); assert.notEqual(result.status,0); assert.match(result.stderr,/Quit WorkBuddy/) + for (let i=0;i<2;i++) { + result=run(); assert.equal(result.status,0,result.stderr+'\n'+result.stdout) + const mcp=JSON.parse(await readFile(join(config,'mcp.json'))), settings=JSON.parse(await readFile(join(config,'settings.json'))) + assert.equal(mcp.mcpServers.other.command,'keep-me'); assert.equal(settings.custom,true) + assert.equal(settings.hooks.Stop.length,2) + assert.match(await readFile(join(config,'skills/opengui/SKILL.md'),'utf8'),/opengui/) + const state=JSON.parse(await readFile(join(config,'opengui/local-install.json'))) + assert.equal(state.version,'0.2.0'); assert(state.backups.every(b=>b.backup===null || b.backup.includes('before-opengui'))) + assert((await readFile(join(state.packageDir,'scripts/install-local.mjs'),'utf8')).includes('mergeHostHooks')) + } + console.log('PASS: real prebuilt npm install, native dependency import, packaged config installer, upgrade, retained foreign MCP/Hooks, paths with spaces and running-host rejection.') +} finally { await rm(temporary,{recursive:true,force:true}) } diff --git a/workbuddy-plugin/scripts/validate.mjs b/workbuddy-plugin/scripts/validate.mjs index cd7f921..019d588 100644 --- a/workbuddy-plugin/scripts/validate.mjs +++ b/workbuddy-plugin/scripts/validate.mjs @@ -1,3 +1,4 @@ +import { execFileSync } from 'node:child_process' import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import { readFile, readdir, stat } from 'node:fs/promises' @@ -8,7 +9,9 @@ import { VERSION } from '../lib/state.js' const root = fileURLToPath(new URL('..', import.meta.url)) const json = async path => JSON.parse(await readFile(join(root, path), 'utf8')) +if (process.platform !== 'win32') execFileSync('bash', ['-n', join(root, 'scripts/install-macos.command')]) const pkg = await json('package.json') +assert((await readFile(join(root, 'scripts/install-macos.command'), 'utf8')).includes('VERSION=' + pkg.version + '\n'), 'Release installer version mismatch') const meta = await json('connector/connector-meta.json') const config = await json('connector/mcp.json') assert.equal(pkg.name, 'opengui-mcp') From 4a8c4a9c4f3cc85b9457d43e9f92e32b4a2efe20 Mon Sep 17 00:00:00 2001 From: harveysang Date: Mon, 7 Sep 2026 09:58:59 +0800 Subject: [PATCH 2/4] feat(release): publish plugin testing prereleases with stable gates --- .github/workflows/opengui-codex-release.yml | 3 ++- .github/workflows/workbuddy-plugin-ci.yml | 1 + .../workflows/workbuddy-plugin-release.yml | 11 ++++++++- plugins/opengui/README.md | 3 +++ plugins/opengui/docs/release-notes.md | 22 ++++++++---------- skills/opengui-plugin-install/SKILL.md | 2 +- workbuddy-plugin/README.md | 13 +++++++++-- workbuddy-plugin/README.zh-CN.md | 6 +++++ workbuddy-plugin/docs/release-notes.md | 11 +++++++++ workbuddy-plugin/scripts/publish.mjs | 6 +++-- workbuddy-plugin/scripts/test-publish.mjs | 23 +++++++++++++++++++ 11 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 workbuddy-plugin/docs/release-notes.md create mode 100644 workbuddy-plugin/scripts/test-publish.mjs diff --git a/.github/workflows/opengui-codex-release.yml b/.github/workflows/opengui-codex-release.yml index 1ab2693..b0bb2b5 100644 --- a/.github/workflows/opengui-codex-release.yml +++ b/.github/workflows/opengui-codex-release.yml @@ -79,5 +79,6 @@ jobs: "release-assets/$archive" "release-assets/$archive.sha256" \ "release-assets/$upload" "release-assets/$upload.sha256" \ "release-assets/$installer" "release-assets/$installer.sha256" --clobber - # Keep the release draft until real-device acceptance is recorded. + # Stable publication requires real-device acceptance. + # The maintainer may explicitly publish this draft as a testing prerelease. # Directory submission, approval, and publication are separate manual gates. diff --git a/.github/workflows/workbuddy-plugin-ci.yml b/.github/workflows/workbuddy-plugin-ci.yml index 80cc914..c740dfa 100644 --- a/.github/workflows/workbuddy-plugin-ci.yml +++ b/.github/workflows/workbuddy-plugin-ci.yml @@ -48,6 +48,7 @@ jobs: cache-dependency-path: workbuddy-plugin/package-lock.json - run: npm ci - run: npm run pack:release + - run: node scripts/test-publish.mjs - run: node scripts/test-release-installer.mjs - run: npm run test:native - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/workbuddy-plugin-release.yml b/.github/workflows/workbuddy-plugin-release.yml index a10d83d..482b0a1 100644 --- a/.github/workflows/workbuddy-plugin-release.yml +++ b/.github/workflows/workbuddy-plugin-release.yml @@ -1,6 +1,12 @@ name: WorkBuddy Plugin Release on: + workflow_dispatch: + inputs: + prerelease: + description: Publish for public testing (stable requires real-device acceptance) + type: boolean + default: true push: tags: - 'opengui-workbuddy-v*' @@ -16,6 +22,8 @@ jobs: release: runs-on: macos-latest timeout-minutes: 20 + env: + OPENGUI_PRERELEASE: ${{ github.event_name == 'push' || inputs.prerelease }} defaults: run: working-directory: workbuddy-plugin @@ -33,7 +41,8 @@ jobs: - run: npm ci - run: npm run pack:release - run: npm run smoke:packed - - name: Require real WorkBuddy acceptance + - name: Require real WorkBuddy acceptance for stable publication + if: env.OPENGUI_PRERELEASE != 'true' run: node scripts/validate.mjs --release - name: Publish immutable WorkBuddy assets env: diff --git a/plugins/opengui/README.md b/plugins/opengui/README.md index fafa426..05e894f 100644 --- a/plugins/opengui/README.md +++ b/plugins/opengui/README.md @@ -129,3 +129,6 @@ Mac setup, actual Android tasks on one and two devices, both architectures, fina downloaded archive checksums, public policy URLs, and publisher identity. GitHub artifact creation is not OpenAI approval. Do not announce publication until the approved version has actually been published. + +Public testing releases are explicitly marked prerelease on GitHub. Publishing a testing +prerelease does not complete desktop/device acceptance or authorize stable/directory publication. diff --git a/plugins/opengui/docs/release-notes.md b/plugins/opengui/docs/release-notes.md index b12ad34..3235256 100644 --- a/plugins/opengui/docs/release-notes.md +++ b/plugins/opengui/docs/release-notes.md @@ -1,15 +1,11 @@ # OpenGUI for Codex 0.1.0 -Initial standalone candidate, independent of the production DSH plugin. - -- Local macOS Android control and a read-only multi-device wall. -- Frozen device selection, one-action screenshot freshness, and operation limits. -- Observe-only sessions, session discovery, cancellation, idle expiry, and private - screenshot cleanup. -- Verified first-use Node runtime setup and pinned scrcpy Unicode input. -- Native confirmation for classified send/publish/purchase/delete actions. -- Separate package, dependencies, build, tests, and release version line. - -Automated verification, real-device acceptance, GitHub release, directory review, -and public publication are distinct gates. This document does not claim that any -of the latter gates have completed. +Public testing prerelease for local Codex on macOS arm64/x64. + +- Install the prebuilt plugin with a host-specific installer that downloads and verifies private Node, with no source build or Xcode. +- Control authorized Android devices from screenshots, or view a read-only device wall. +- Keep existing plugin settings and previous packages, with explicit conflict checks and source recovery during upgrades. + +Download `opengui-codex-0.1.0-install.command` and its `.sha256`, verify the checksum, then run the installer with `bash`. Codex CLI with plugin support is required. Start a new Codex chat after installation and first request read-only device discovery. + +This prerelease is for testing. Automated tests and isolated installer checks have passed locally; desktop, real-phone and two-device acceptance remain incomplete. It is not a stable or directory-approved release. diff --git a/skills/opengui-plugin-install/SKILL.md b/skills/opengui-plugin-install/SKILL.md index fe6c078..d73fdbc 100644 --- a/skills/opengui-plugin-install/SKILL.md +++ b/skills/opengui-plugin-install/SKILL.md @@ -7,7 +7,7 @@ description: Install or upgrade the standalone OpenGUI Codex or WorkBuddy plugin Infer Codex or WorkBuddy from the request; ask only if the target is missing. The two plugins have independent versions and installation directories. Do not install both unless requested. -1. Read the public GitHub releases API for `Core-Mate/OpenGUI`, following pagination. Select the newest non-draft, non-prerelease semantic version with tag `opengui-codex-vX.Y.Z` or `opengui-workbuddy-vX.Y.Z` and all matching assets below. An explicit requested version takes precedence; never silently substitute a different version. Do not use the repository-wide latest release, which may be DSH or an APK. +1. Read the public GitHub releases API for `Core-Mate/OpenGUI`, following pagination. Select the newest non-draft, non-prerelease semantic version with tag `opengui-codex-vX.Y.Z` or `opengui-workbuddy-vX.Y.Z` and all matching assets below. If the user explicitly asks for the public testing/prerelease version, include non-draft prereleases for that host and explain their uncompleted device acceptance. An explicit requested version takes precedence; never silently substitute a different version. Do not use the repository-wide latest release, which may be DSH or an APK. 2. Download `opengui-HOST-X.Y.Z-install.command` and its `.sha256` asset from that release into a fresh temporary directory. Use HTTPS with redirect-to-HTTPS only. Require exact filenames and verify SHA-256 before executing the installer. The checksum establishes integrity relative to the selected public release, not an independent publisher signature. 3. Finish existing OpenGUI tasks before an upgrade. WorkBuddy must be closed before its configuration can be changed; do not kill it or its phone/mirror processes. Codex requires the native CLI with `codex plugin` support. Never remove a conflicting plugin source without the user's authorization. 4. Run `bash /absolute/path/opengui-HOST-X.Y.Z-install.command`. It downloads and verifies the matching package, prepares private Node 22.23.2, installs into a fresh version directory, and configures only the selected host. No Git clone, pnpm, source compilation, or user-run test suite is required. diff --git a/workbuddy-plugin/README.md b/workbuddy-plugin/README.md index 69fc0a6..6ed6e55 100644 --- a/workbuddy-plugin/README.md +++ b/workbuddy-plugin/README.md @@ -98,14 +98,15 @@ Candidate tag convention: `opengui-workbuddy-v0.2.0` (not created by local insta - `dist/opengui-mcp-0.2.0.tgz` and `.sha256` - `dist/opengui-workbuddy-connector-0.2.0.zip` and `.sha256` +- `dist/opengui-workbuddy-0.2.0-install.command` and `.sha256` The ZIP contains `opengui/connector-meta.json`, `mcp.json`, `icon.svg`, and `skills/control/SKILL.md`. Its npx command pins the matching GitHub Release tarball. Do not distribute this candidate manifest as installable until that asset exists. The tarball includes code, ADB, notices, and package metadata; npm resolves its pinned runtime dependencies. No npm publish step is required. -The WorkBuddy-only workflows do not modify the DSH/Codex pipelines. Publishing additionally requires all real-host acceptance entries in `release-readiness.json` to be verified with evidence. A successful build, local archive, pushed commit, GitHub Release, and WorkBuddy marketplace approval are distinct states. Release assets are immutable; a rerun compares existing bytes and fails rather than replacing mismatched files. +The WorkBuddy-only workflows do not modify the DSH/Codex pipelines. Stable publication additionally requires all real-host acceptance entries in `release-readiness.json` to be verified with evidence. A successful build, local archive, pushed commit, GitHub Release, and WorkBuddy marketplace approval are distinct states. Release assets are immutable; a rerun compares existing bytes and fails rather than replacing mismatched files. Submit the verified connector ZIP to the WorkBuddy team separately. See the official [connector format](https://open.workbuddy.cn/docs/connector) and [Skill format](https://open.workbuddy.cn/docs/skill). -## Acceptance before release +## Acceptance before stable release 1. Load the candidate in the real WorkBuddy client, confirm eleven tools and actual images available to the selected model. 2. Verify tap/swipe/ASCII and Unicode text/key/launch/wait on an authorized test phone. Never test payment, publication, or deletion on real accounts. @@ -115,3 +116,11 @@ Submit the verified connector ZIP to the WorkBuddy team separately. See the offi 6. Run packaged startup on all claimed desktop platforms. Record real-host evidence before marking the release gates verified. See [NOTICE.md](NOTICE.md) for the fixed public-source provenance and third-party notices. + +### Public testing and stable releases + +Namespaced tag pushes publish an explicitly marked GitHub prerelease with the verified +prebuilt assets. This testing lane does not mark any manual acceptance item as passed. +Stable publication uses a manual workflow dispatch on that same version tag with +`prerelease=false`; all existing `release-readiness.json` checks and evidence remain required. +Published assets remain immutable in both lanes. The public directory is a separate approval. diff --git a/workbuddy-plugin/README.zh-CN.md b/workbuddy-plugin/README.zh-CN.md index 661ad88..ca8c6c2 100644 --- a/workbuddy-plugin/README.zh-CN.md +++ b/workbuddy-plugin/README.zh-CN.md @@ -87,3 +87,9 @@ bash opengui-workbuddy-0.2.0-install.command --archive /绝对路径/opengui-mcp 自动测试、归档包和标准 MCP 冒烟检查不等于真实 WorkBuddy 验收。`release-readiness.json` 中的宿主图片接入、真机动作、双机隔离、自动续跑和停止恢复等项目全部验收后,专属发布流程才允许创建 GitHub Release。WorkBuddy 市场提交与审核另行进行。 完整的接口流程、验证清单、隐私说明和来源见 [英文 README](README.md)、[Skill](connector/skills/control/SKILL.md) 和 [来源说明](NOTICE.md)。 + +### 公开试用版与稳定版 + +独立版本标签触发的发布标记为 GitHub 预发布版,供公开试用,不修改人工验收记录。 +稳定版需在同一标签上手动运行发布工作流并设置 `prerelease=false`,仍须通过全部真实宿主和设备验收。 +两条发布路径都保留不可变安装资产;市场上架另行审核。 diff --git a/workbuddy-plugin/docs/release-notes.md b/workbuddy-plugin/docs/release-notes.md new file mode 100644 index 0000000..6902a5b --- /dev/null +++ b/workbuddy-plugin/docs/release-notes.md @@ -0,0 +1,11 @@ +# OpenGUI for WorkBuddy 0.2.0 + +Public testing prerelease for WorkBuddy 5.5.3 or newer on macOS arm64/x64. + +- Install the prebuilt package, private Node, MCP, Skill and lifecycle Hooks through one installer, without a source build or Xcode. +- Control authorized Android devices from screenshots while keeping a separate read-only phone mirror available. +- Continue unfinished tasks through host lifecycle Hooks and preserve unrelated configuration and previous packages during upgrades. + +Finish existing phone tasks, close their mirrors and quit WorkBuddy. Download `opengui-workbuddy-0.2.0-install.command` and its `.sha256`, verify the checksum, then run the installer with `bash`. Reopen WorkBuddy, trust the OpenGUI MCP and select `/opengui`; first request read-only device discovery. + +This prerelease is for testing. Automated tests, packaged startup and an isolated installation without system Node have passed locally. Real WorkBuddy desktop, phone actions, two-device conflicts and host stop/continuation acceptance remain incomplete. It is not a stable or marketplace-approved release. diff --git a/workbuddy-plugin/scripts/publish.mjs b/workbuddy-plugin/scripts/publish.mjs index 1aaf620..3b7105f 100644 --- a/workbuddy-plugin/scripts/publish.mjs +++ b/workbuddy-plugin/scripts/publish.mjs @@ -9,7 +9,8 @@ const root = fileURLToPath(new URL('..', import.meta.url)) const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) const tag = `opengui-workbuddy-v${version}` assert.equal(process.env.GITHUB_REF_NAME, tag, 'Only publish the exact independent WorkBuddy version tag') -execFileSync(process.execPath, [join(root, 'scripts/validate.mjs'), '--release'], { cwd: root, stdio: 'inherit' }) +const prerelease = process.env.OPENGUI_PRERELEASE === 'true' +execFileSync(process.execPath, [join(root, 'scripts/validate.mjs'), ...(prerelease ? [] : ['--release'])], { cwd: root, stdio: 'inherit' }) const assets = [`opengui-mcp-${version}.tgz`, `opengui-workbuddy-connector-${version}.zip`, `opengui-workbuddy-${version}-install.command`].flatMap(name => [name, `${name}.sha256`]) const gh = args => execFileSync('gh', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) let exists = false @@ -24,8 +25,9 @@ if (exists) { assert((await readFile(join(root, 'dist', name))).equals(await readFile(join(temporary, name))), `Published asset differs: ${name}; use a new version, never overwrite`) } } finally { await rm(temporary, { recursive: true, force: true }) } + if (!prerelease) gh(['release', 'edit', tag, '--prerelease=false']) console.log('Existing immutable release assets match.') } else { - gh(['release', 'create', tag, ...assets.map(name => join(root, 'dist', name)), '--verify-tag', '--title', `OpenGUI for WorkBuddy ${version}`, '--notes-file', join(root, 'CHANGELOG.md')]) + gh(['release', 'create', tag, ...assets.map(name => join(root, 'dist', name)), '--verify-tag', ...(prerelease ? ['--prerelease', '--latest=false'] : []), '--title', `OpenGUI for WorkBuddy ${version}`, '--notes-file', join(root, 'docs/release-notes.md')]) console.log(`Published ${tag}. WorkBuddy marketplace review is a separate step.`) } diff --git a/workbuddy-plugin/scripts/test-publish.mjs b/workbuddy-plugin/scripts/test-publish.mjs new file mode 100644 index 0000000..0eb89c2 --- /dev/null +++ b/workbuddy-plugin/scripts/test-publish.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +const temp = await mkdtemp(join(tmpdir(), 'opengui-publish-test-')) +try { + const output = join(temp, 'args.json') + await writeFile(join(temp, 'gh'), `#!${process.execPath} +const fs=require('fs');const a=process.argv.slice(2);if(a[1]==='view'){console.error('release not found');process.exit(1)}fs.writeFileSync(process.env.PUBLISH_TEST_OUTPUT,JSON.stringify(a)); +`, {mode:0o755}) + const env={...process.env,PATH:temp+':'+process.env.PATH,PUBLISH_TEST_OUTPUT:output,GITHUB_REF_NAME:'opengui-workbuddy-v0.2.0'} + const script=fileURLToPath(new URL('./publish.mjs', import.meta.url)) + let result=spawnSync(process.execPath,[script],{env:{...env,OPENGUI_PRERELEASE:'false'},encoding:'utf8'}) + assert.notEqual(result.status,0);assert.match(result.stderr,/Unverified release gate/) + result=spawnSync(process.execPath,[script],{env:{...env,OPENGUI_PRERELEASE:'true'},encoding:'utf8'}) + assert.equal(result.status,0,result.stderr) + const args=JSON.parse(await readFile(output,'utf8')) + assert(args.includes('--prerelease'));assert(args.includes('--latest=false')) + assert(args.some(a=>a.endsWith('opengui-workbuddy-0.2.0-install.command.sha256'))) + console.log('PASS: stable publication remains blocked by missing acceptance; public testing uses prerelease and installer assets.') +} finally { await rm(temp,{recursive:true,force:true}) } From 0e81380464ccb7ae0d3336975081b76460562505 Mon Sep 17 00:00:00 2001 From: harveysang Date: Mon, 7 Sep 2026 10:00:02 +0800 Subject: [PATCH 3/4] docs(plugins): distinguish testing downloads from stable publication --- plugins/opengui/README.md | 4 ++-- workbuddy-plugin/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/opengui/README.md b/plugins/opengui/README.md index 05e894f..17a8772 100644 --- a/plugins/opengui/README.md +++ b/plugins/opengui/README.md @@ -1,7 +1,7 @@ # OpenGUI for Codex Standalone screenshot-guided Android control for **local Codex on macOS arm64/x64**. -This is a candidate package, not a published or directory-approved release. +This is a testing candidate, not a stable or directory-approved release. It includes a control Skill, local CLI/daemon, macOS ADB executable, and a read-only device wall. It does not depend on, modify, install, update, or reload DSH. @@ -20,7 +20,7 @@ Start a **new chat**, choose OpenGUI, and ask to list connected phones without o For agent-assisted installation, use the repository's [installation Skill](../../skills/opengui-plugin-install/SKILL.md) and say “Install OpenGUI for Codex”. The Skill resolves only complete releases for this host. -There is no public standalone download until the release gates below are satisfied. +Public testing downloads are marked prerelease; stable publication requires the release gates below. The installer uses the independent `opengui-standalone` marketplace and leaves the repository's legacy marketplace unchanged. Finish OpenGUI tasks before upgrading. diff --git a/workbuddy-plugin/README.md b/workbuddy-plugin/README.md index 6ed6e55..9be7a7a 100644 --- a/workbuddy-plugin/README.md +++ b/workbuddy-plugin/README.md @@ -2,7 +2,7 @@ [中文说明](README.zh-CN.md) -Independent local **MCP + Skill + lifecycle Hooks** connector for autonomous Android control, native read-only mirroring, and a read-only device wall. Version `0.2.0` (broker protocol `7`) is a local candidate, not a published release or a marketplace-approved connector. +Independent local **MCP + Skill + lifecycle Hooks** connector for autonomous Android control, native read-only mirroring, and a read-only device wall. Version `0.2.0` (broker protocol `7`) is a testing candidate, not a stable release or a marketplace-approved connector. Every OpenGUI request begins with `opengui_start`, displaying all connected authorized phones without taking control locks. Windows are read-only and silent, and persist across task completion, cancellation and MCP recycling. Only user-requested closure or device/runtime failure ends them. Phone tasks use the current WorkBuddy VLM in a screenshot–action–screenshot loop; standalone viewing sends no images to the model. On macOS the bundled helper verifies initial window visibility and renderer readiness once per control task. Subsequent minimization, occlusion, desktop switching, closure or renderer exit does not revoke control: the model receives independent phone screenshots. Initial display failure is reported and blocks operation until startup succeeds; it is never silently bypassed. First use downloads verified scrcpy into the independent WorkBuddy cache. From b415f7bf13632d62261412c923611507a6434a7a Mon Sep 17 00:00:00 2001 From: harveysang Date: Mon, 7 Sep 2026 10:04:43 +0800 Subject: [PATCH 4/4] fix(install): handle Windows checkout line endings --- .gitattributes | 3 +++ plugins/opengui/scripts/validate.mjs | 2 +- workbuddy-plugin/scripts/validate.mjs | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c6add7f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ + +# Keep executable macOS installers usable after Windows checkouts. +*.command text eol=lf diff --git a/plugins/opengui/scripts/validate.mjs b/plugins/opengui/scripts/validate.mjs index 31afaf6..776cb05 100644 --- a/plugins/opengui/scripts/validate.mjs +++ b/plugins/opengui/scripts/validate.mjs @@ -10,7 +10,7 @@ import { stagePlugin } from './stage.mjs' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const json = async path => JSON.parse(await readFile(join(root, path), 'utf8')) const pkg = await json('package.json') -assert((await readFile(join(root, 'scripts/install-macos.command'), 'utf8')).includes('VERSION=' + pkg.version + '\n'), 'Release installer version mismatch') +assert((await readFile(join(root, 'scripts/install-macos.command'), 'utf8')).split(/\r?\n/).includes('VERSION=' + pkg.version), 'Release installer version mismatch') execFileSync('/bin/bash', ['-n', join(root, 'scripts/install-macos.command')]) const plugin = await json('.codex-plugin/plugin.json') assert.equal(plugin.name, 'opengui') diff --git a/workbuddy-plugin/scripts/validate.mjs b/workbuddy-plugin/scripts/validate.mjs index 019d588..feae0f8 100644 --- a/workbuddy-plugin/scripts/validate.mjs +++ b/workbuddy-plugin/scripts/validate.mjs @@ -11,7 +11,7 @@ const root = fileURLToPath(new URL('..', import.meta.url)) const json = async path => JSON.parse(await readFile(join(root, path), 'utf8')) if (process.platform !== 'win32') execFileSync('bash', ['-n', join(root, 'scripts/install-macos.command')]) const pkg = await json('package.json') -assert((await readFile(join(root, 'scripts/install-macos.command'), 'utf8')).includes('VERSION=' + pkg.version + '\n'), 'Release installer version mismatch') +assert((await readFile(join(root, 'scripts/install-macos.command'), 'utf8')).split(/\r?\n/).includes('VERSION=' + pkg.version), 'Release installer version mismatch') const meta = await json('connector/connector-meta.json') const config = await json('connector/mcp.json') assert.equal(pkg.name, 'opengui-mcp')