From cli-tools
部署 Swift CLI 工具(編譯 universal binary、建立 GitHub Release、安裝到 ~/bin)。在 Swift CLI 專案目錄中使用。
npx claudepluginhub psychquant/psychquant-claude-plugins --plugin cli-toolsThis skill is limited to using the following tools:
完整的 CLI 工具部署流程:版本更新 → 編譯 universal binary → GitHub Release。
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
完整的 CLI 工具部署流程:版本更新 → 編譯 universal binary → GitHub Release。
重要:必須先 bump version 再 build,否則 binary 內嵌的版本號會是舊的。
$1 = 版本號(可選,如 1.0.0)pwd
ls Package.swift
必須存在:Package.swift 且包含 executableTarget
重要:Binary 名稱由 products: 區塊的 .executable(name: ...) 決定,不是 targets: 區塊的 executableTarget.name。兩者可以不同(例如 .executable(name: "gfh", targets: ["gfs"]) — binary 是 gfh,target 資料夾是 gfs)。抓錯的話後面 lipo / curl 會找不到檔案。
# 優先從 products 區塊抓 .executable(name: "X")
BINARY_NAME=$(grep -E '\.executable\(name:' Package.swift | head -1 | sed 's/.*name: *"\([^"]*\)".*/\1/')
# Fallback:只有當沒有 products 區塊時才用 executableTarget 名稱
if [ -z "$BINARY_NAME" ]; then
BINARY_NAME=$(grep -A2 'executableTarget' Package.swift | grep 'name:' | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
fi
echo "Binary: $BINARY_NAME"
若 BINARY_NAME 為空,停下來請使用者確認 — 繼續往下做會寫壞路徑。
搜尋版本號來源(依優先順序):
Sources/*/Version.swift — 找 static let current = "x.x.x" 或類似git describe --tags --abbrev=0 2>/dev/null0.1.0如果提供了 $1,使用該版本。否則用 AskUserQuestion 詢問,預設 patch bump。
找到版本號所在的檔案並更新:
Sources/*/Version.swift — 更新版本字串先檢查是否已有 [Unreleased] 區塊:
[Unreleased] 區塊 → 把標題改成 [{version}] - {date}(promote 既有內容,避免重複):
## [Unreleased] → ## [{version}] - {date}
[Unreleased] 區塊 → 在 CHANGELOG.md 頂部新增:
## [{version}] - {date}
### Added
- ...
### Changed
- ...
### Fixed
- ...
在沒有 [Unreleased] 的情況下,用 AskUserQuestion 詢問變更摘要。
如果 README.md 有 Version History 區塊,加入新版本。
rm -rf .build 2>/dev/null || true
swift build -c release --arch arm64
swift build -c release --arch x86_64
mkdir -p .release
lipo -create \
.build/arm64-apple-macosx/release/$BINARY_NAME \
.build/x86_64-apple-macosx/release/$BINARY_NAME \
-output .release/$BINARY_NAME
# 清除 xattr 汙染(Dropbox 目錄 build 的 binary 帶 com.dropbox.attrs)
xattr -cr .release/$BINARY_NAME
# 重新簽名(lipo 破壞原始 code signature)
codesign --force --sign - .release/$BINARY_NAME
file .release/$BINARY_NAME
lipo -info .release/$BINARY_NAME
.release/$BINARY_NAME version 2>/dev/null || .release/$BINARY_NAME --version 2>/dev/null
預期:
Mach-O universal binary with 2 architectures: [x86_64] [arm64]{version}(如果顯示舊版本,表示 Phase 1 的 version bump 沒生效,停下來檢查)只 stage 本次 deploy 實際動過的檔案 — git add -A 會把使用者 working tree 裡無關的 modified files(例如 session-start hook 改的 .claude/*、CLAUDE.md)全部塞進 release commit,污染歷史。
# 只 stage Phase 1 改過的檔案
git add Sources/*/Version.swift CHANGELOG.md
[ -f README.md ] && git diff --cached --quiet README.md 2>/dev/null || git add README.md 2>/dev/null || true
# 檢查 staging 內容,確認沒混進無關檔案
git diff --cached --stat
git commit -m "v{version}: {change-summary}"
git push origin main
如果 git diff --cached --stat 顯示了非預期的檔案,停下來檢查 — 可能是 working tree 本來就髒。
OWNER_REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
# 建立 Release
RELEASE_ID=$(gh api repos/$OWNER_REPO/releases --method POST \
-f tag_name="v{version}" \
-f target_commitish="main" \
-f name="v{version}" \
-f body="{release-notes}" \
-F draft=false \
-F prerelease=false \
--jq '.id')
echo "Release created: ID=$RELEASE_ID"
# 上傳 Binary(用 curl,支援進度條)
TOKEN=$(gh auth token)
curl -L -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
"https://uploads.github.com/repos/$OWNER_REPO/releases/$RELEASE_ID/assets?name=$BINARY_NAME" \
--data-binary "@.release/$BINARY_NAME" \
--progress-bar -o /dev/null -w "Upload: HTTP %{http_code}, %{size_upload} bytes\n"
# 驗證
gh release view v{version} --json assets --jq '.assets[] | "\(.name) (\(.size) bytes)"'
Release Notes 模板:
## Install
\```bash
curl -fsSL https://github.com/{owner}/{repo}/releases/latest/download/{binary} -o ~/bin/{binary} && chmod +x ~/bin/{binary}
\```
---
{CHANGELOG 內容}
cp .release/$BINARY_NAME ~/bin/$BINARY_NAME
chmod +x ~/bin/$BINARY_NAME
驗證:
~/bin/$BINARY_NAME version 2>/dev/null || ~/bin/$BINARY_NAME --version 2>/dev/null
rm -rf .release
# CLI Deploy 完成
- **專案**: {project-name}
- **版本**: v{version}
- **Binary**: {binary-name} (universal: arm64 + x86_64)
- **Release**: https://github.com/{owner}/{repo}/releases/tag/v{version}
- **本地**: ~/bin/{binary-name}
安裝指令(其他使用者):
curl -fsSL https://github.com/{owner}/{repo}/releases/latest/download/{binary} -o ~/bin/{binary} && chmod +x ~/bin/{binary}
| 變更類型 | 版本變更 | 範例 |
|---|---|---|
| 新功能 | MINOR +1 | 1.0.0 → 1.1.0 |
| Bug 修復 | PATCH +1 | 1.0.0 → 1.0.1 |
| 破壞性變更 | MAJOR +1 | 1.0.0 → 2.0.0 |
| 問題 | 解決方案 |
|---|---|
| Dropbox 衝突導致 build 失敗 | rm -rf .build 後重新編譯 |
| lipo 失敗 | 確認兩種架構都編譯成功 |
| gh release create 卡住 | 用 gh api + curl 分步驟上傳 |
| codesign 失敗 | codesign --force --sign - 用 ad-hoc 簽名 |
| binary 版本號是舊的 | 確認 Phase 1 已更新 Version.swift 再 build(不能先 build 再 bump) |