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
2 changes: 2 additions & 0 deletions Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@
"USD" = "USD";
"Usage" = "Usage";
"Updates" = "Updates";
"Local customization build" = "Local customization build";
"Official automatic updates are disabled to preserve local features. Use the sync script for updates." = "Official automatic updates are disabled to preserve local features. Use the sync script for updates.";
"VALUE" = "VALUE";
"Warn" = "Warn";
"Warning" = "Warning";
Expand Down
2 changes: 2 additions & 0 deletions Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@
"USD" = "USD";
"Usage" = "用量";
"Updates" = "更新";
"Local customization build" = "本地定制版";
"Official automatic updates are disabled to preserve local features. Use the sync script for updates." = "已关闭官方自动更新,以保留本地功能。请使用同步脚本更新。";
"VALUE" = "价值";
"Warn" = "警告";
"Warning" = "警告";
Expand Down
15 changes: 12 additions & 3 deletions Sources/Update/UpdaterController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,30 @@ final class UpdaterController: ObservableObject {
static let shared = UpdaterController()

private let controller: SPUStandardUpdaterController
let updatesEnabled: Bool

@Published var automaticallyChecks: Bool {
didSet { controller.updater.automaticallyChecksForUpdates = automaticallyChecks }
didSet {
guard updatesEnabled else {
if automaticallyChecks { automaticallyChecks = false }
return
}
controller.updater.automaticallyChecksForUpdates = automaticallyChecks
}
}

private init() {
updatesEnabled = Bundle.main.object(forInfoDictionaryKey: "CodexIslandLocalBuild") as? Bool != true
controller = SPUStandardUpdaterController(
startingUpdater: true,
startingUpdater: updatesEnabled,
updaterDelegate: nil,
userDriverDelegate: nil
)
automaticallyChecks = controller.updater.automaticallyChecksForUpdates
automaticallyChecks = updatesEnabled && controller.updater.automaticallyChecksForUpdates
}

func checkForUpdates() {
guard updatesEnabled else { return }
controller.checkForUpdates(nil)
}
}
35 changes: 23 additions & 12 deletions Sources/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,19 +434,30 @@ struct SettingsView: View {
private var updatesSection: some View {
VStack(alignment: .leading, spacing: 0) {
sectionLabel("Updates")
SettingsRow(
title: "Check for updates automatically",
subtitle: "Check for new versions in the background and notify you when one's available."
) {
SettingsToggle(isOn: updater.automaticallyChecks) {
updater.automaticallyChecks.toggle()
if updater.updatesEnabled {
SettingsRow(
title: "Check for updates automatically",
subtitle: "Check for new versions in the background and notify you when one's available."
) {
SettingsToggle(isOn: updater.automaticallyChecks) {
updater.automaticallyChecks.toggle()
}
}
SettingsRow(
title: "Check now",
subtitle: "Look for a new version immediately."
) {
PillButton(label: "Check") { updater.checkForUpdates() }
}
} else {
SettingsRow(
title: "Local customization build",
subtitle: "Official automatic updates are disabled to preserve local features. Use the sync script for updates."
) {
Image(systemName: "checkmark.shield.fill")
.font(Typography.label)
.foregroundStyle(.green)
}
}
SettingsRow(
title: "Check now",
subtitle: "Look for a new version immediately."
) {
PillButton(label: "Check") { updater.checkForUpdates() }
}
}
.padding(.horizontal, 14)
Expand Down
22 changes: 19 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ SPARKLE_FW="$SPARKLE_DIR/Sparkle.framework"
SU_PUBLIC_KEY="bz1gwLBKgIL/Y7OO23o3gaMNIeTpvv/C90F9inr9Quo="

SU_FEED_URL="${SU_FEED_URL:-https://github.com/ericjypark/codex-island/releases/latest/download/appcast.xml}"
ENABLE_UPDATES="${ENABLE_UPDATES:-0}"
if [[ "$ENABLE_UPDATES" != "0" && "$ENABLE_UPDATES" != "1" ]]; then
echo "error: ENABLE_UPDATES must be 0 or 1" >&2
exit 1
fi

if [[ "$ENABLE_UPDATES" == "1" ]]; then
SPARKLE_PLIST_CONFIG="
<key>SUFeedURL</key><string>$SU_FEED_URL</string>
<key>SUPublicEDKey</key><string>$SU_PUBLIC_KEY</string>
<key>SUEnableAutomaticChecks</key><true/>"
else
# A locally customized build must not install an official binary over itself.
# Upstream source updates are merged by scripts/sync-upstream.sh instead.
SPARKLE_PLIST_CONFIG="
<key>CodexIslandLocalBuild</key><true/>
<key>SUEnableAutomaticChecks</key><false/>"
fi

rm -rf "$BUILD_DIR"
mkdir -p "$MACOS_DIR" "$RES_DIR" "$FRAMEWORKS_DIR"
Expand Down Expand Up @@ -92,9 +110,7 @@ cat > "$CONTENTS/Info.plist" <<EOF
<key>NSHighResolutionCapable</key><true/>
<key>NSPrincipalClass</key><string>NSApplication</string>
<key>NSHumanReadableCopyright</key><string>Copyright © 2026 Eric Park. MIT licensed.</string>
<key>SUFeedURL</key><string>$SU_FEED_URL</string>
<key>SUPublicEDKey</key><string>$SU_PUBLIC_KEY</string>
<key>SUEnableAutomaticChecks</key><true/>
$SPARKLE_PLIST_CONFIG
</dict>
</plist>
EOF
Expand Down
59 changes: 59 additions & 0 deletions docs/UPSTREAM_SYNC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Keeping local features when upstream updates

Run the synchronization script from the repository root whenever CodexIsland
publishes a new version:

```bash
./scripts/sync-upstream.sh
```

The script fetches `origin/main`, creates a `codex/backup-before-upstream-*`
backup branch, merges instead of replacing local history, and runs the test
suite before committing. To build the app as part of the same operation:

```bash
SYNC_BUILD_APP=1 ./scripts/sync-upstream.sh
```

If Git reports conflicts, review and stage the resolved files, then run:

```bash
./scripts/sync-upstream.sh --continue
```

Use `./scripts/sync-upstream.sh --abort` to return to the exact pre-merge state.
The backup branch remains available even after a successful merge.

Local builds intentionally omit the official Sparkle update feed. Otherwise an
official release could replace the customized binary after a restart. The
release workflow explicitly enables Sparkle only for official distribution
builds. Your preferences remain in the normal CodexIsland UserDefaults domain.

## 上游更新时保留本地功能

CodexIsland 发布新版本后,在项目根目录执行:

```bash
./scripts/sync-upstream.sh
```

脚本会获取 `origin/main`,自动创建
`codex/backup-before-upstream-*` 备份分支,通过合并保留本地历史,
并在提交前运行测试。如果希望同时构建应用:

```bash
SYNC_BUILD_APP=1 ./scripts/sync-upstream.sh
```

如果发生冲突,脚本会停下,不会覆盖任何本地代码。解决冲突并将文件
加入暂存区后,执行:

```bash
./scripts/sync-upstream.sh --continue
```

如果希望取消本次合并,执行 `./scripts/sync-upstream.sh --abort`。

本地定制构建会有意关闭官方 Sparkle 更新源,避免重启后官方原版覆盖定制功能。
只有正式发布脚本才会启用 Sparkle。外观、服务显示和任务提醒等偏好仍保存在
CodexIsland 原有的 UserDefaults 中。
2 changes: 1 addition & 1 deletion release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ if ! command -v create-dmg >/dev/null 2>&1; then
exit 1
fi

./build.sh
ENABLE_UPDATES=1 ./build.sh

rm -rf "$DIST"
mkdir -p "$DIST"
Expand Down
151 changes: 151 additions & 0 deletions scripts/sync-upstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/bin/bash
# Merge the latest upstream release into the current customization branch.
# This script never resets or overwrites local history. It requires a clean
# worktree, creates a backup branch, and pauses safely when conflicts occur.

set -euo pipefail

cd "$(dirname "$0")/.."

UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-origin}"
UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-main}"
ACTION="${1:-sync}"

usage() {
cat <<'EOF'
Usage:
./scripts/sync-upstream.sh Fetch, test, and merge upstream/main
./scripts/sync-upstream.sh --continue Continue after resolving conflicts
./scripts/sync-upstream.sh --abort Abort the current upstream merge

Optional environment variables:
UPSTREAM_REMOTE=origin
UPSTREAM_BRANCH=main
SYNC_SKIP_TESTS=1
SYNC_BUILD_APP=1
SYNC_SDKROOT=/path/to/MacOSX.sdk
EOF
}

run_validation() {
if [[ "${SYNC_SKIP_TESTS:-0}" != "1" ]]; then
echo "Running tests..."
local sdk="${SYNC_SDKROOT:-}"
if [[ -z "$sdk" && -d /Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk ]]; then
sdk=/Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk
fi
if [[ -n "$sdk" ]]; then
SDKROOT="$sdk" \
CLANG_MODULE_CACHE_PATH="${TMPDIR:-/tmp}/codex-island-module-cache" \
./scripts/run-tests.sh
else
CLANG_MODULE_CACHE_PATH="${TMPDIR:-/tmp}/codex-island-module-cache" \
./scripts/run-tests.sh
fi
fi

if [[ "${SYNC_BUILD_APP:-0}" == "1" ]]; then
echo "Building CodexIsland..."
./build.sh
fi
}

case "$ACTION" in
--help|-h)
usage
exit 0
;;
--abort)
if git rev-parse -q --verify MERGE_HEAD >/dev/null; then
git merge --abort
echo "Upstream merge aborted; the pre-sync branch is still available."
else
echo "error: no upstream merge is in progress" >&2
exit 1
fi
exit 0
;;
--continue)
if ! git rev-parse -q --verify MERGE_HEAD >/dev/null; then
echo "error: no upstream merge is in progress" >&2
exit 1
fi
if [[ -n "$(git diff --name-only --diff-filter=U)" ]]; then
echo "error: unresolved conflicts remain" >&2
git diff --name-only --diff-filter=U >&2
exit 1
fi
run_validation
git commit --no-edit
Comment on lines +78 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unstaged fixes before validating the merge

If a user stages a conflict resolution and then adjusts it again—especially after a validation failure—run_validation tests the newer working-tree contents, but git commit --no-edit records the older index contents and still reports success. As confirmed by git commit -h, -a/--all is the option to “commit all changed files”; this call neither uses it nor verifies that the working tree matches the index, so the resulting merge can differ from what passed validation.

Useful? React with 👍 / 👎.

echo "Upstream merge completed successfully."
exit 0
;;
sync)
;;
*)
usage >&2
exit 2
;;
esac

if git rev-parse -q --verify MERGE_HEAD >/dev/null; then
echo "error: a merge is already in progress; use --continue or --abort" >&2
exit 1
fi

if [[ -n "$(git status --porcelain)" ]]; then
echo "error: commit or stash local changes before syncing" >&2
exit 1
fi

current_branch="$(git symbolic-ref --quiet --short HEAD || true)"
if [[ -z "$current_branch" ]]; then
echo "error: switch to your customization branch before syncing" >&2
exit 1
fi

echo "Fetching ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}..."
git fetch "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH" --tags
target="${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"

if git merge-base --is-ancestor "$target" HEAD; then
echo "Already up to date with ${target}."
run_validation
exit 0
fi

stamp="$(date +%Y%m%d-%H%M%S)"
short_head="$(git rev-parse --short HEAD)"
backup_branch="codex/backup-before-upstream-${stamp}-${short_head}"
git branch "$backup_branch" HEAD
echo "Backup created: ${backup_branch}"

if ! git merge --no-ff --no-commit "$target"; then
cat >&2 <<EOF

The merge paused because conflicts need review. Your local work is safe.
Resolve the listed files, stage them with git add, then run:
./scripts/sync-upstream.sh --continue

To restore the pre-merge state, run:
./scripts/sync-upstream.sh --abort

Backup branch: ${backup_branch}
EOF
exit 1
fi

if ! run_validation; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve validation failures before committing

When a clean merge causes run-tests.sh to fail, invoking run_validation as the condition of if ! disables Bash's set -e behavior throughout the function. The function then reaches the false SYNC_BUILD_APP check, returns success, and the script commits the unvalidated merge and reports success. Capture the function's status explicitly or make each validation command return immediately on failure.

Useful? React with 👍 / 👎.

cat >&2 <<EOF

Validation failed, so the merge has not been committed.
Fix the issue and run ./scripts/sync-upstream.sh --continue,
or run ./scripts/sync-upstream.sh --abort.
Backup branch: ${backup_branch}
EOF
exit 1
fi

git commit --no-edit
echo "Merged ${target} into ${current_branch}."
echo "Backup branch: ${backup_branch}"
46 changes: 46 additions & 0 deletions 同步上游并构建.command
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash

set -u

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR" || exit 1

clear
echo "========================================"
echo " CodexIsland 上游同步与本地构建"
echo "========================================"
echo
echo "将会:"
echo " 1. 获取官方最新版本"
echo " 2. 创建本地备份分支"
echo " 3. 合并更新并保留本地功能"
echo " 4. 运行测试并构建新应用"
echo

if SYNC_BUILD_APP=1 ./scripts/sync-upstream.sh; then
echo
echo "✓ 处理完成"
echo
echo "应用位置:"
echo "$SCRIPT_DIR/build/CodexIsland.app"
echo
echo "可以打开 build 文件夹查看新版本。"
else
status=$?
echo
echo "✗ 未能自动完成(错误代码:$status)"
echo
if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
echo "检测到合并冲突,本地功能没有被覆盖。"
echo "解决冲突后可执行:"
echo " ./scripts/sync-upstream.sh --continue"
echo
echo "如需取消这次合并:"
echo " ./scripts/sync-upstream.sh --abort"
else
echo "请查看上方提示;项目文件不会被强制覆盖。"
fi
fi

echo
read -r -p "按 Enter 键关闭窗口…"
Loading