From 895655097bddf466f9daa20eb92dd2c8d6655114 Mon Sep 17 00:00:00 2001 From: mlboy Date: Thu, 12 Nov 2026 17:23:35 +0800 Subject: [PATCH 1/2] test(workbench): give real-git taskChanges tests a Windows-capable env Windows git needs SystemRoot/USERPROFILE/TEMP and rejects \\.\nul as GIT_CONFIG_GLOBAL; the bare PATH-only env failed git init on the v1.0.37 Windows release leg. Use an empty mkdtemp config file and surface git stderr on setup failures. Signed-off-by: mlboy --- .../workbench/__tests__/taskChanges.test.ts | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/main/workbench/__tests__/taskChanges.test.ts b/apps/desktop/src/main/workbench/__tests__/taskChanges.test.ts index e23ec99..2f2b5f7 100644 --- a/apps/desktop/src/main/workbench/__tests__/taskChanges.test.ts +++ b/apps/desktop/src/main/workbench/__tests__/taskChanges.test.ts @@ -2,7 +2,7 @@ import { execFile, execFileSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { GIT_SAFE_CONFIG, isCommitId, @@ -11,7 +11,7 @@ import { readHeadCommit, readTaskChanges, splitPatch, - type GitExec, + type GitExecResult, } from '../taskChanges'; describe('task changes parsing', () => { @@ -56,7 +56,33 @@ function gitAvailable(): boolean { } } -const realExec: GitExec = (args) => +/** + * The few host variables git needs to start. Windows git fails without SystemRoot / + * USERPROFILE / TEMP; everything else (the user's git config included) stays out. + */ +function hostEnv(): Record { + const env: Record = {}; + for (const name of ['PATH', 'HOME', 'SystemRoot', 'windir', 'USERPROFILE', 'TEMP', 'TMP', 'PATHEXT', 'ComSpec']) { + const value = process.env[name]; + if (value) env[name] = value; + } + return env; +} + +// An empty file, not os.devNull: Windows git rejects \\.\nul as a config path. +let emptyGlobalConfig = ''; +let globalConfigDir = ''; +beforeAll(() => { + globalConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aiopt-taskchanges-cfg-')); + emptyGlobalConfig = path.join(globalConfigDir, 'gitconfig'); + fs.writeFileSync(emptyGlobalConfig, ''); +}); +afterAll(() => { + fs.rmSync(globalConfigDir, { recursive: true, force: true }); +}); + +/** A GitExec that also keeps stderr, so a failed setup step says why. */ +const realExec: (args: readonly string[]) => Promise = (args) => new Promise((resolve, reject) => { execFile( 'git', @@ -64,19 +90,19 @@ const realExec: GitExec = (args) => { encoding: 'utf8', env: { - PATH: process.env.PATH ?? '', + ...hostEnv(), GIT_CONFIG_NOSYSTEM: '1', - GIT_CONFIG_GLOBAL: os.devNull, + GIT_CONFIG_GLOBAL: emptyGlobalConfig, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@example.com', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@example.com', }, }, - (err, stdout) => { + (err, stdout, stderr) => { const code = err && typeof (err as { code?: unknown }).code === 'number' ? (err as { code: number }).code : 0; if (err && typeof (err as { code?: unknown }).code !== 'number') reject(err); - else resolve({ code, stdout }); + else resolve({ code, stdout, stderr }); }, ); }); @@ -85,7 +111,7 @@ describe.skipIf(!gitAvailable())('readTaskChanges (real git)', () => { let dir = ''; const run = async (...args: string[]) => { const out = await realExec(['-C', dir, ...args]); - if (out.code !== 0) throw new Error(`git ${args.join(' ')} failed`); + if (out.code !== 0) throw new Error(`git ${args.join(' ')} failed: ${out.stderr ?? ''}`); }; beforeEach(async () => { From a5241737a0175669d49d45e483b80a9b6bc0e34a Mon Sep 17 00:00:00 2001 From: mlboy Date: Thu, 12 Nov 2026 17:23:35 +0800 Subject: [PATCH 2/2] docs: add the brew trust step to the Homebrew install route Recent Homebrew refuses casks from an untrusted third-party tap. Signed-off-by: mlboy --- README.ja.md | 4 ++++ README.ko.md | 4 ++++ README.md | 5 +++++ README.zh-CN.md | 4 ++++ docs/dev-rules/development-workflow.md | 4 +++- 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.ja.md b/README.ja.md index 4170ad8..d0272b2 100644 --- a/README.ja.md +++ b/README.ja.md @@ -130,9 +130,13 @@ Hermes、OpenCode、pi**。そのうち 8 つはプロバイダーにバイン **macOS(Apple Silicon)は Homebrew でのインストールをおすすめします。** ```sh +brew tap rushteam/tap +brew trust --cask rushteam/tap/aiopt brew install --cask rushteam/tap/aiopt ``` +新しい Homebrew は信頼していないサードパーティ tap の cask を読み込まないため、先に `brew trust` を実行します。 + この cask は **AiOpt.app** を `/Applications` に置き、macOS の隔離フラグを消すので、初回起動時に Gatekeeper の 確認は出ません。`brew upgrade --cask aiopt` で新しいリリースに更新し、`brew uninstall --cask aiopt` でアプリを 削除します。`--zap` を付けると、保存済みのキーを含むデータも削除されます。 diff --git a/README.ko.md b/README.ko.md index 1fb5ae1..41601c7 100644 --- a/README.ko.md +++ b/README.ko.md @@ -125,9 +125,13 @@ Hermes, OpenCode, pi**. 그중 8개는 공급자에 바인딩할 수 있습니 **macOS(Apple Silicon)는 Homebrew로 설치하는 것을 권장합니다.** ```sh +brew tap rushteam/tap +brew trust --cask rushteam/tap/aiopt brew install --cask rushteam/tap/aiopt ``` +최신 Homebrew는 신뢰하지 않은 서드파티 tap의 cask를 불러오지 않으므로 먼저 `brew trust`를 실행합니다. + 이 cask는 **AiOpt.app**을 `/Applications`에 설치하고 macOS 격리 플래그를 지우므로, 첫 실행 때 Gatekeeper 확인 창이 뜨지 않습니다. `brew upgrade --cask aiopt`로 새 릴리스로 업그레이드하고, `brew uninstall --cask aiopt`로 앱을 제거합니다. `--zap`을 붙이면 저장된 키를 포함한 데이터도 삭제됩니다. diff --git a/README.md b/README.md index 0c4fd4f..8a3ab4a 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,14 @@ adopts `~/.agents/skills` when that directory already exists. **macOS (Apple Silicon): install with Homebrew.** This is the recommended route: ```sh +brew tap rushteam/tap +brew trust --cask rushteam/tap/aiopt brew install --cask rushteam/tap/aiopt ``` +Recent Homebrew refuses casks from a third-party tap until you trust them, hence the +`brew trust` step. + The cask puts **AiOpt.app** in `/Applications` and clears the macOS quarantine flag, so the first launch shows no Gatekeeper prompt. `brew upgrade --cask aiopt` moves to a new release; `brew uninstall --cask aiopt` removes the app, and adding `--zap` also deletes its data, diff --git a/README.zh-CN.md b/README.zh-CN.md index b8c7da8..e0fce78 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -109,9 +109,13 @@ AiOpt 用一个统一的供应商管理入口、每个 Agent 一个切换开关 **macOS(Apple Silicon)推荐用 Homebrew 安装:** ```sh +brew tap rushteam/tap +brew trust --cask rushteam/tap/aiopt brew install --cask rushteam/tap/aiopt ``` +新版 Homebrew 不会加载未信任的第三方 tap 里的 cask,所以需要先执行 `brew trust`。 + 该 cask 会把 **AiOpt.app** 装进 `/Applications` 并清除 macOS 的隔离标记,首次启动不会弹出 Gatekeeper 提示。 `brew upgrade --cask aiopt` 升级到新版本;`brew uninstall --cask aiopt` 卸载应用,加上 `--zap` 还会删除它的 数据,包括已保存的密钥。 diff --git a/docs/dev-rules/development-workflow.md b/docs/dev-rules/development-workflow.md index ae9d2f0..63cc108 100644 --- a/docs/dev-rules/development-workflow.md +++ b/docs/dev-rules/development-workflow.md @@ -95,7 +95,9 @@ To release: on `main` here and runs `brew style`, `brew audit --cask --strict --online`, and a real install. A change of only `version` and `sha256` is committed to the tap's `main` directly. Any other difference opens a pull request in the tap for review. The cask is the primary - macOS install route: `brew install --cask rushteam/tap/aiopt`. + macOS install route: `brew tap rushteam/tap`, `brew trust --cask rushteam/tap/aiopt`, then + `brew install --cask rushteam/tap/aiopt` (recent Homebrew refuses an untrusted tap's casks, + so the sync workflow trusts the cask before its audit too). So change the cask's structure (url, postflight, zap, caveats) here, through a PR. The `version` and `sha256` in this repository's copy may lag the tap and need no bump. Drafts