Compare commits

..
27 Commits
Author SHA1 Message Date
james.maes 634dc9f688 Merge pull request 'ci: build on the shared ci-workflows macOS workflow (build only)' (#2) from ci/shared-macos into master
macos / macos (push) Successful in 51s
build / macos (push) Successful in 51s
2026-11-08 23:33:40 +00:00
James MaesandClaude Opus 5.5 da5779f872 fix: capture weakly in the outer resolve closures (Swift 6.3 ImplicitStrongCapture)
macos / macos (push) Successful in 52s
build / macos (push) Successful in 52s
The background closures in ExceptionsTab.applyContent and
ExceptionEditorView.resolveHeaderAsync captured row/self strongly while the
inner main-queue closure asked for weak; Xcode 27's Swift diagnoses the
mismatch (#ImplicitStrongCapture), an error under warnings-as-errors. Both
closures now capture weakly, so neither keeps the row or view alive while
the app info resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:32:05 -06:00
James MaesandClaude Opus 5.5 874971aa47 ci: one-line Release build command
macos / macos (push) Failing after 17s
build / macos (push) Failing after 17s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:30:44 -06:00
James MaesandClaude Opus 5.5 756943f2b5 ci: compile the Release scheme at macOS 12.0 on Xcode 27
macos / macos (push) Failing after 2s
build / macos (push) Failing after 2s
grogu has only Xcode 27.2, which rejects the 10.13 deployment target. The
CI build is a compile check at 12.0 with the deprecation group downgraded
(as config/test-base.xcconfig); the shipping build stays on the GitHub
mirror with Xcode 26.0.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:29:44 -06:00
James MaesandClaude Opus 5.5 c675f1817f ci: select Xcode 26.0.1, as ci_cd.yml does
macos / macos (push) Failing after 1s
build / macos (push) Failing after 2s
Xcode 27.2 rejects the app's macOS 10.13 deployment target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:28:37 -06:00
James MaesandClaude Opus 5.5 c2be713446 ci: build on the shared ci-workflows macOS workflow (build only)
macos / macos (push) Failing after 6s
build / macos (push) Failing after 6s
Gitea CI on grogu (kof-macos-arm64): Guards B and C from guard.yml (the
de-paywall marker and if-false wrap in computeState(), no conflict markers),
an unsigned Release build, and build-for-testing of the Test scheme. The
tests (Guard A) are not run on CI: the runner has no GUI session for
testmanagerd; docs/contributing.md documents running scripts/run_tests.sh
locally before merging.

config/test-base.xcconfig takes the unit-test-only Xcode 27 fix from PR #1
(test target 12.0, deprecation group downgraded, every other warning still
an error); the shipping app keeps 10.13. The .github workflows stay for the
GitHub mirror.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:27:40 -06:00
semantic-release-bot dc40fa43bc chore(release): 100.2.1 [skip ci]
## [100.2.1](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.2.0...v100.2.1) (2026-06-16)
2026-06-16 21:21:18 +00:00
james.maes 2ae7ded357 docs: record auto-update live, cask v100.2.0, release-gate decouple
Reflect v100.2.0 (auto-update via the GitHub Pages feed), the Homebrew cask bump, and the will-release gating that lets non-release pushes build and test without cutting a release.
2026-06-16 16:16:42 -05:00
james.maes dd15f22031 ci: gate release apparatus on semantic-release will-release decision
determine_next_version.sh writes an empty VERSION.txt when no commit since the last tag is release-worthy; gate cert-import/sign-build/notarize/appcast/release (and version injection) on that, so non-release pushes build+test without cutting a release or needing [skip ci].
2026-06-16 16:11:32 -05:00
semantic-release-bot 4ecfc4606f chore(release): 100.2.0 [skip ci]
# [100.2.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.1.0...v100.2.0) (2026-06-16)

### Features

* **updates:** enable Sparkle auto-update via GitHub Pages appcast ([45457cb](https://github.com/KofTwentyTwo/CommandTabFree/commit/45457cb))
2026-06-16 21:05:45 +00:00
james.maes 45457cb313 feat(updates): enable Sparkle auto-update via GitHub Pages appcast
Point DOMAIN at the repo's GitHub Pages site (serves the gh-pages appcast.xml), enable SUEnableAutomaticChecks, and remove the in-tree appcast skeleton (feed is published out-of-tree). Ships an auto-updating build.
2026-06-16 16:02:04 -05:00
james.maes 62e0226f01 ci: stop release tail clobbering .fork-sync-state cursor [skip ci]
Reset .fork-sync-state to v11.3.0 (last-synced upstream tag) and remove the release-tail step that overwrote it with the fork release tag, which would stall upstream-sync. Update SESSION-STATE/TODO/CICD-STATUS/README for the live signed+notarized v100.1.0 release.
2026-06-16 15:32:55 -05:00
github-actions[bot] 92254c021f chore(sync-state): advance to v100.1.0 [skip ci] 2026-06-16 19:48:28 +00:00
semantic-release-bot 0aca4d9400 chore(release): 100.1.0 [skip ci]
# [100.1.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.0.3...v100.1.0) (2026-06-16)

### Bug Fixes

* **ci:** build ad-hoc until Apple cert provisioned ([6070f0c](https://github.com/KofTwentyTwo/CommandTabFree/commit/6070f0c))
* **l10n:** regenerate base Localizable.strings from source ([d1e1b29](https://github.com/KofTwentyTwo/CommandTabFree/commit/d1e1b29))

### Features

* **release:** sign + notarize with fork Developer ID ([2f9fac1](https://github.com/KofTwentyTwo/CommandTabFree/commit/2f9fac1))
2026-06-16 19:48:19 +00:00
james.maes 2f9fac17b7 feat(release): sign + notarize with fork Developer ID
Set the fork Developer ID in config/local.xcconfig (Manual style + team), re-sign the vendored Sparkle helpers under it, and import the cert with an optional APPLE_P12_PASSWORD. Built, signed, and notarized (Accepted) locally before cutting.
2026-06-16 14:42:45 -05:00
james.maes 4a007e88c9 docs: add owner-gated TODO; tidy SESSION-STATE
Capture the owner-gated remaining work (publishing pipeline blockers) in docs/TODO.md; make SESSION-STATE HEAD reference hash-stable.
2026-06-16 12:55:22 -05:00
james.maes 885c52567d docs: record CI green + ad-hoc build fix + sync labels
Push CI is fully green (run 27634828800). NEXT SESSION blocker resolved; remaining work is owner-gated only.
2026-06-16 12:16:12 -05:00
james.maes 6070f0cbb9 fix(ci): build ad-hoc until Apple cert provisioned
Release build inherited upstream's Developer ID and failed on CI (exit 65, no cert). Build ad-hoc when the release gate is not signing-ready; build_app.sh now forwards xcodebuild overrides. Auto-switches to signed when the owner adds the cert.
2026-06-16 12:08:05 -05:00
james.maes d1e1b29623 fix(l10n): regenerate base Localizable.strings from source
Base strings drifted from source after the About redesign and brand sweep; regenerating via genstrings unblocks the generated-files CI gate. Dead paywall strings stay upstream-branded to keep merge surface minimal.
2026-06-15 15:22:49 -05:00
james.maes 2588101096 docs: correct CI/CD status + add NEXT SESSION handoff 2026-06-15 15:07:12 -05:00
james.maes 6313c295f3 docs: record CI/CD wiring + Guard B fix (green on unblocked) 2026-06-15 14:55:24 -05:00
james.maes d3e7529d91 ci: fix Guard B to match shipped de-paywall patch (guard-false-else + depaywall marker) 2026-06-15 14:52:09 -05:00
james.maes 287c4a07c4 ci: gate release tail on owner secrets (push CI green until then); sign_update --ed-key-file; commitlint sync-merge ignores; drop upstream-only scripts 2026-06-15 14:45:14 -05:00
james.maes eb95ad2d52 docs(readme): note .dmg/.zip direct-download install option 2026-06-15 14:31:18 -05:00
james.maes b9e871f176 chore: repo renamed alt-tab-free → CommandTabFree (update URLs); drop JetBrains sponsor from README 2026-06-15 14:13:53 -05:00
james.maes 39f10cfba3 docs: mark ⌘⇥ app icon as final (owner-approved), drop real-artwork TODO 2026-06-15 14:05:16 -05:00
james.maes 5b590bd6e5 docs: README install section + mark §3.1 brand/l10n/icon items done 2026-06-15 14:00:35 -05:00
31 changed files with 391 additions and 251 deletions

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
# Gitea CI on the shared KofTwentyTwo/ci-workflows macOS workflow (grogu). The .github workflows keep
# running on the GitHub mirror (release, notarization, appcast); this one is build only and unsigned.
#
# Guard B the de-paywall marker and the if-false wrap are still in computeState() (docs/PLAN-maintained-fork.md §4.4)
# Guard C no unresolved merge conflict markers in the tree
# build the Release scheme, unsigned (a compile check at macOS 12.0: grogu has only Xcode 27)
# Guard A the Test scheme is compiled (build-for-testing) but not run: the grogu runner has no GUI session for
# testmanagerd. Run scripts/run_tests.sh locally before merging (docs/contributing.md).
name: build
on:
push:
branches: ['**']
jobs:
macos:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/macos.yaml@v1
with:
commands: |
F=src/pro/license/LicenseManager.swift
[ -f "$F" ] || { echo "::error::Guard B: $F not found; chokepoint moved? Refresh the de-paywall patch (§6.2)."; exit 1; }
grep -q 'depaywall' "$F" || { echo "::error::PAYWALL RELOCKED: 'depaywall' marker missing from $F (§6.2)."; exit 1; }
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' "$F" || { echo "::error::PAYWALL RELOCKED: 'if false { … }' wrap missing from $F (§6.2)."; exit 1; }
echo "Guard B OK"
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
echo "::error::UNRESOLVED CONFLICT MARKERS present; resolve before merge (§4.4 Guard C)."; exit 1
fi
echo "Guard C OK"
# grogu has only Xcode 27.2, which rejects the app's 10.13 deployment target (the shipping build is the GitHub
# mirror's, on Xcode 26.0.1). This compile check builds at 12.0 with the same deprecation downgrade as
# config/test-base.xcconfig; every other Swift warning stays an error.
xcodebuild -quiet -project alt-tab-macos.xcodeproj -scheme Release -derivedDataPath DerivedData CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO MACOSX_DEPLOYMENT_TARGET=12.0 SWIFT_TREAT_WARNINGS_AS_ERRORS=NO 'OTHER_SWIFT_FLAGS=$(inherited) -warnings-as-errors -Wwarning DeprecatedDeclaration' build
xcodebuild -quiet -project alt-tab-macos.xcodeproj -scheme Test -configuration Release -derivedDataPath DerivedData build-for-testing
+93 -37
View File
@@ -30,6 +30,9 @@ env:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_P12_CERTIFICATE: ${{ secrets.APPLE_P12_CERTIFICATE }}
# alt-tab-free [depaywall]: optional .p12 export password (empty if exported password-less).
# Not part of the release-gate AND — the cert import tolerates an empty password.
APPLE_P12_PASSWORD: ${{ secrets.APPLE_P12_PASSWORD }}
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
GITHUB_EVENT_AFTER: ${{ github.event.after }}
@@ -68,17 +71,44 @@ jobs:
# just before action-gh-release as belt-and-suspenders.
- name: anti-relock — verify de-paywall marker present (Guard B)
run: |
grep -q 'alt-tab-free \[depaywall\]' src/pro/license/LicenseManager.swift \
grep -q 'depaywall' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: depaywall marker missing from computeState()"; exit 1; }
# The marker proves the patch is present; pair with the if-false wrap the patch introduces.
# A bare `return .pro` grep is NOT load-bearing (it matches the unpatched line 187 too).
grep -Eq 'if[[:space:]]+false[[:space:]]*\{' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: 'if false { ... }' wrap missing from computeState()"; exit 1; }
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: 'guard false else' (forced .pro) missing from computeState()"; exit 1; }
- name: anti-conflict — no unresolved merge markers (Guard C)
run: |
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
echo "UNRESOLVED CONFLICT MARKERS present — resolve before merge/release"; exit 1
fi
# alt-tab-free [depaywall]: release-readiness gate (PLAN §3.3/§4.3 owner-secret checklist).
# The fork ships unsigned/ad-hoc until the OWNER provisions the Apple Developer ID cert and the
# Sparkle key. We do NOT hard-fail when those secrets are absent — that would make every push
# RED on a fork that is correctly built but not yet signing-provisioned. Instead we compute a
# boolean from secret presence and SKIP the sign/notarize/appcast/release tail cleanly when it
# is false. The build + Guard A (run_tests.sh) still run on push, so push CI is GREEN today and
# the release tail FLIPS ON automatically the moment the owner adds the secrets. Secrets are not
# available in step-level `if:` directly, so map them to env and emit an output here.
- name: release-gate — are signing/notarization secrets present?
id: release-gate
env:
HAS_P12: ${{ secrets.APPLE_P12_CERTIFICATE != '' }}
HAS_APPLE_ID: ${{ secrets.APPLE_ID != '' }}
HAS_APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD != '' }}
HAS_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID != '' }}
HAS_SPARKLE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY != '' }}
run: |
set -euo pipefail
if [ "$HAS_P12" = "true" ] && [ "$HAS_APPLE_ID" = "true" ] \
&& [ "$HAS_APPLE_PASSWORD" = "true" ] && [ "$HAS_APPLE_TEAM_ID" = "true" ] \
&& [ "$HAS_SPARKLE_KEY" = "true" ]; then
echo "signing-ready=true" >> "$GITHUB_OUTPUT"
echo "All signing/notarization/Sparkle secrets present — release tail WILL run on push."
else
echo "signing-ready=false" >> "$GITHUB_OUTPUT"
echo "::warning::Signing/notarization/Sparkle secrets absent — release tail (codesign/notarize/appcast/semantic-release/gh-release) is SKIPPED. Build + Guard A still run. Provision APPLE_P12_CERTIFICATE / APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID / SPARKLE_ED_PRIVATE_KEY in the 'production' Environment to enable releases (PLAN §3.3/§3.4)."
fi
- uses: actions/setup-node@v6
with:
node-version: 16 # PLAN §4.3: keep node-16; the package.json engines mismatch only warns
@@ -104,37 +134,73 @@ jobs:
# meaningless and can mis-bump, so gate to push (PLAN §4.2).
- run: scripts/determine_next_version.sh
if: github.event_name == 'push'
# alt-tab-free [depaywall]: decouple the release apparatus from "every signing-ready push".
# determine_next_version.sh writes an EMPTY VERSION.txt when no commit since the last tag is
# release-worthy (semantic-release dry-run finds no bump). Gate the sign/notarize/release steps
# on this so a docs/ci/chore push still runs guards + commitlint + run_tests (Release-config
# compile check) but does NOT sign-build, notarize, or cut a release — no [skip ci] needed (§4.3).
- name: release-decision — did semantic-release compute a version?
id: release-decision
if: github.event_name == 'push'
run: |
if [ -n "$(tr -d '[:space:]' < "$VERSION_FILE" 2>/dev/null)" ]; then
echo "will-release=true" >> "$GITHUB_OUTPUT"
echo "Release-worthy: semantic-release will cut v$(cat "$VERSION_FILE")."
else
echo "will-release=false" >> "$GITHUB_OUTPUT"
echo "::notice::No release-worthy commit — build/test only; notarize+release tail skipped."
fi
# Inject the release version only when releasing; an empty CURRENT_PROJECT_VERSION would crash
# App.version (see config/local.xcconfig). Non-release builds use the committed 0.0.0 default.
- run: scripts/replace_environment_variables_in_app.sh
if: github.event_name == 'push'
if: github.event_name == 'push' && steps.release-decision.outputs.will-release == 'true'
# codesign needs APPLE_P12_CERTIFICATE — gated on signing-ready so a not-yet-provisioned fork
# does not RED on the missing-cert import (PLAN §3.3/§4.3).
- run: scripts/codesign/setup_ci_master.sh
if: github.event_name == 'push'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# Guard A — the anti-relock unit test (appended to LicenseManagerTests.swift). Runs on both
# push and PR; on the push path it sits before the sign/tag steps below (PLAN §4.4).
# push and PR (no secrets needed); on the push path it sits before the sign/tag steps below
# so a relock fails BEFORE any irreversible side-effect (PLAN §4.4). This is what keeps push
# CI GREEN today on the signing-unprovisioned fork.
- run: scripts/run_tests.sh
- run: scripts/build_app.sh
if: github.event_name == 'push'
# alt-tab-free [depaywall]: build the Release product to prove the tree compiles. The fork
# inherits upstream's Developer ID via config/release.xcconfig (CODE_SIGN_IDENTITY), which is
# absent on CI until the owner provisions the Apple cert — so when the release gate is NOT
# signing-ready we build AD-HOC (CODE_SIGN_IDENTITY=-, signing not required, no timestamp) to
# prove the tree compiles without a cert. When the owner adds the secrets AND sets the fork's
# Developer ID in config/local.xcconfig, this builds signed for the release tail (PLAN §3.3/§4.3).
- name: build app (signed — owner cert provisioned)
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
run: scripts/build_app.sh
- name: build app (ad-hoc — until signing-provisioned)
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready != 'true'
run: scripts/build_app.sh CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO OTHER_CODE_SIGN_FLAGS=--timestamp=none
# --- Release tail: requires the owner's Apple Developer ID + Sparkle secrets. Skipped cleanly
# (NOT failed) when they are absent, so push CI stays GREEN until the owner provisions them. ---
- run: scripts/package_and_notarize_release.sh
if: github.event_name == 'push'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: upload_symbols_to_appcenter.sh REMOVED — telemetry is off (PLAN §3.4).
- run: scripts/update_appcast.sh
if: github.event_name == 'push'
- run: scripts/update_readme_and_website.sh
if: github.event_name == 'push'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: update_readme_and_website.sh REMOVED — it queried the UPSTREAM
# lwouis/alt-tab-macos GitHub API for stars/downloads and rewrote docs/contributors.md from the
# UPSTREAM contributor graph (semantic mismatch for the fork) and dispatched the lwouis website.
# Cosmetic only; dropped per PLAN §4.0/§4.3 (alongside update_website.sh, already removed).
- run: npx semantic-release
if: github.event_name == 'push'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
- id: set_release_info
run: scripts/extract_latest_changelog.sh
if: github.event_name == 'push'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: belt-and-suspenders copy of Guard B + Guard C immediately before the
# release is created. The load-bearing copy is at the top of the job (pre-tag-push); this second
# copy guards the gh-release object itself (PLAN §4.4).
- name: anti-relock + anti-conflict re-check (belt-and-suspenders)
if: github.event_name == 'push'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
run: |
grep -q 'alt-tab-free \[depaywall\]' src/pro/license/LicenseManager.swift \
grep -q 'depaywall' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: depaywall marker missing from computeState()"; exit 1; }
grep -Eq 'if[[:space:]]+false[[:space:]]*\{' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: 'if false { ... }' wrap missing from computeState()"; exit 1; }
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: 'guard false else' (forced .pro) missing from computeState()"; exit 1; }
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
echo "UNRESOLVED CONFLICT MARKERS present — resolve before release"; exit 1
fi
@@ -142,29 +208,19 @@ jobs:
# (release.config.js has no @semantic-release/github plugin, so softprops is the sole release
# creator — PLAN §4.3). repository defaults to ${{ github.repository }} = the fork.
- uses: softprops/action-gh-release@v3
if: github.event_name == 'push'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
with:
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ steps.set_release_info.outputs.tag_name }}
body: ${{ steps.set_release_info.outputs.body }}
files: ${{ env.XCODE_BUILD_PATH }}/*.zip
# alt-tab-free [depaywall]: advance the committed last-synced-tag state to the released tag and
# commit it, so the upstream-sync cron syncs the OLDEST upstream tag strictly newer than this —
# not "newest tag minus --is-ancestor". This makes resolve-in-order hold by construction and
# prevents a closed-unmerged sync PR from silently dropping commits (PLAN §4.2). Skipped with
# [skip ci] so it does not re-trigger the workflow.
- name: advance .fork-sync-state to the released tag
if: github.event_name == 'push'
run: |
tag="${{ steps.set_release_info.outputs.tag_name }}"
[ -n "$tag" ] || { echo "no tag_name output — skipping .fork-sync-state advance"; exit 0; }
printf '%s\n' "$tag" > .fork-sync-state
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if ! git diff --quiet -- .fork-sync-state; then
git add .fork-sync-state
git commit -m "chore(sync-state): advance to $tag [skip ci]"
git push origin "HEAD:${{ github.ref_name }}"
fi
# alt-tab-free [depaywall]: the release tail intentionally does NOT advance .fork-sync-state.
# That file is the UPSTREAM-sync cursor — the last-synced UPSTREAM tag (e.g. v11.3.0), read by
# upstream_sync.yml to pick "the oldest upstream tag strictly newer than it." The fork versions
# independently (semantic-release cuts v100.x), so a prior revision's step that wrote the FORK
# release tag here set the cursor to v100.x — after which no upstream tag is ever "newer" and
# upstream-sync would silently never fire. The cursor must instead be advanced by the
# upstream-sync merge flow, recording the UPSTREAM tag it synced (to be wired with
# upstream_sync.yml — PLAN §4.2). Until then .fork-sync-state stays at the last real upstream tag.
# alt-tab-free [depaywall]: update_website.sh (lwouis website dispatch) REMOVED (PLAN §3.4/§4.2).
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
echo "::error::PAYWALL RELOCKED: 'depaywall' marker missing from $F — the de-paywall flip was lost in a merge. Refresh the patch (§6.2)."
exit 1
}
grep -Eq 'if[[:space:]]+false[[:space:]]*\{' "$F" || {
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' "$F" || {
echo "::error::PAYWALL RELOCKED: 'if false { … }' wrap missing from $F — the de-paywall body-suppress was lost. Refresh the patch (§6.2)."
exit 1
}
+3
View File
@@ -20,3 +20,6 @@ codesign.p12
!/.claude/skills/
/scripts/l10n/missing-translations.json
/ai/output/
# alt-tab-free [depaywall]: the Sparkle feed is published OUT OF TREE to the gh-pages branch by
# scripts/update_appcast.sh; the in-tree appcast.xml is removed (PLAN §3.3).
/appcast.xml
+2 -2
View File
@@ -64,8 +64,8 @@
Auto-checks stay off (below) until DOMAIN serves a real appcast host. -->
<string>1YfoCJDk/T8GQx1Gpf3O4ApFgDkBiN6oe/z/Kfnxs/M=</string>
<key>SUEnableAutomaticChecks</key>
<!-- alt-tab-free: off until DOMAIN serves a real appcast (avoids failed update checks). -->
<string>false</string>
<!-- alt-tab-free: enabled 2026-06-16 — DOMAIN serves the appcast on GitHub Pages (gh-pages). -->
<string>true</string>
<key>SUScheduledCheckInterval</key>
<string>604800</string>
+14 -3
View File
@@ -12,6 +12,19 @@ CommandTabFree is itself released under the **GNU General Public License v3.0**
license as upstream. See [`LICENCE.md`](LICENCE.md) for the full text and
[`NOTICE.md`](NOTICE.md) for the GPL §5(a) statement of changes made in this fork.
## Install
```sh
brew install --cask koftwentytwo/tap/commandtabfree
```
CommandTabFree is **signed and notarized** (as of v100.1.0), so it launches cleanly through
Gatekeeper — no right-click-**Open** workaround needed. Install via the Homebrew cask above, or
download the **`.dmg`** (or `.zip`) from the
[Releases](https://github.com/KofTwentyTwo/CommandTabFree/releases) page and drag CommandTabFree
to Applications. On first launch, grant **Accessibility** and **Screen Recording** in System
Settings. Universal binary (Apple Silicon + Intel), macOS 10.13+.
## Based on AltTab by Louis Pontoise
This software is built on AltTab (`lwouis/alt-tab-macos`) by Louis Pontoise and its
@@ -27,7 +40,7 @@ The complete corresponding source for every released binary is this repository a
matching release tag:
```
https://github.com/KofTwentyTwo/alt-tab-free
https://github.com/KofTwentyTwo/CommandTabFree
```
For a given release `vX.Y.Z`, the source is the repository tree at that tag, including
@@ -46,6 +59,4 @@ deliberately not conveyed; GPL-3.0 does not require these to be distributed.
CommandTabFree is a free, GPL-3.0 fork of <a href="https://github.com/lwouis/alt-tab-macos">AltTab</a> by Louis Pontoise.
<a href="https://jb.gg/OpenSource"><img src="docs/readme/sponsor.svg" alt="Sponsored by JetBrains" width="900"/></a>
</div>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" standalone="yes"?>
<!--
alt-tab-free [depaywall]: this in-tree appcast.xml has been TRUNCATED to an empty Sparkle feed
skeleton. The fork publishes its real Sparkle feed OUT OF TREE (the gh-pages branch) via
scripts/update_appcast.sh, so the live entries are NOT served from this tracked file (PLAN §3.3).
OWNER ACTION (before publish): `git rm appcast.xml` and add `/appcast.xml` to .gitignore (the
identity agent owns .gitignore). Serving the feed from the tree would (1) conflict on every
upstream + fork release and (2) re-introduce lwouis-key-signed entries the fork's SUPublicEDKey
cannot verify. It is left present-but-empty only so the tree stays buildable until removed.
-->
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>alt-tab-free</title>
<language>en</language>
</channel>
</rss>
+22
View File
@@ -1,3 +1,25 @@
## [100.2.1](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.2.0...v100.2.1) (2026-06-16)
# [100.2.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.1.0...v100.2.0) (2026-06-16)
### Features
* **updates:** enable Sparkle auto-update via GitHub Pages appcast ([45457cb](https://github.com/KofTwentyTwo/CommandTabFree/commit/45457cb))
# [100.1.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.0.3...v100.1.0) (2026-06-16)
### Bug Fixes
* **ci:** build ad-hoc until Apple cert provisioned ([6070f0c](https://github.com/KofTwentyTwo/CommandTabFree/commit/6070f0c))
* **l10n:** regenerate base Localizable.strings from source ([d1e1b29](https://github.com/KofTwentyTwo/CommandTabFree/commit/d1e1b29))
### Features
* **release:** sign + notarize with fork Developer ID ([2f9fac1](https://github.com/KofTwentyTwo/CommandTabFree/commit/2f9fac1))
# [11.3.0](https://github.com/lwouis/alt-tab-macos/compare/v11.2.0...v11.3.0) (2026-06-04)
+12
View File
@@ -1,3 +1,15 @@
// alt-tab-free [depaywall]: commitlint config with an `ignores` predicate so a `chore(sync): merge
// upstream` PR (and the upstream commits it drags in through the merge's second parent) does NOT fail
// commitlint and block the release job (PLAN §4.3). ci_cd.yml lints the WHOLE pushed range on push and
// base..head on a PR, both of which include upstream's own (non-conventional) messages on a sync merge.
// `defaultIgnores: true` keeps config-conventional's built-in "Merge …" ignore; the predicates below
// drop the synthetic merge subject and any "Merge …" / "Revert …" line we don't author.
module.exports = {
extends: ['@commitlint/config-conventional'],
defaultIgnores: true,
ignores: [
(msg) => /^chore\(sync\): merge upstream /.test(msg), // our synthetic merge commit (§4.2)
(msg) => /^Merge /.test(msg), // belt-and-suspenders (merge commits)
(msg) => /^Revert /.test(msg), // upstream revert subjects
],
}
+14 -15
View File
@@ -30,24 +30,23 @@ PRODUCT_BUNDLE_IDENTIFIER = com.koftwentytwo.commandtabfree
CURRENT_PROJECT_VERSION = 0.0.0
// --- Hosts (overrides base.xcconfig :20-21) ---
// OWNER-OVERRIDABLE-BEFORE-PUBLISH: fork-owned hosts.
// TODO: replace fork.invalid with the fork's real web host. DOMAIN must serve
// https://<DOMAIN>/appcast.xml (Sparkle feed) and /support, or update checks /
// the in-app Support affordance 404. fork.invalid is a deliberately NON-routable
// placeholder (RFC 6761 reserved .invalid TLD) so nothing phones home before publish.
DOMAIN = fork.invalid
// TODO: API_DOMAIN drives the user-feedback POST (FeedbackWindow). The activate
// fingerprint POST is closed separately at the handleCustomUrl handler (§2/§3.4),
// so this may be made ROUTABLE for feedback without re-arming exfiltration.
// Decide feedback strategy at bootstrap (§3.4 option a/b/c).
// DOMAIN serves the Sparkle appcast at https://<DOMAIN>/appcast.xml. Set 2026-06-16 to the repo's
// GitHub Pages site (gh-pages branch, published by scripts/update_appcast.sh). A kof22.com custom
// domain can be added later — GitHub redirects the github.io URL, so shipped builds keep updating.
// (/support still 404s here — harmless until a support page exists.)
DOMAIN = koftwentytwo.github.io/CommandTabFree
// API_DOMAIN drives the user-feedback POST (FeedbackWindow) — left NON-routable: feedback is deferred
// (§3.4). The activate fingerprint POST is closed separately at the handleCustomUrl handler.
API_DOMAIN = fork.invalid/api
// --- Code signing (overrides release.xcconfig :5) ---
// TODO: fork Developer ID — set to the fork's own signing identity, e.g.
// CODE_SIGN_IDENTITY = Developer ID Application: <Fork Org> (TEAMID)
// Required for notarization/Gatekeeper. Also re-sign vendored Sparkle helpers
// under this same Developer ID (§3.3 / §4.6) or notarization rejects.
// CODE_SIGN_IDENTITY = Developer ID Application: <Fork Org> (TEAMID)
// Fork Developer ID — signed, notarizable builds (PLAN §3.3 / §4.6). Set 2026-06-16.
// The vendored Sparkle helpers (vendor/Sparkle/Helpers/*) are re-signed under this SAME
// identity (a committed change) — required or notarization rejects the nested code.
// Manual style + TEAM_ID so the headless CI runner signs without an Xcode-managed account.
CODE_SIGN_IDENTITY = Developer ID Application: James Maes (2X834TJ5MA)
CODE_SIGN_STYLE = Manual
TEAM_ID = 2X834TJ5MA
// --- Telemetry off (overrides base.xcconfig; AppCenter no-ops on empty secret) ---
// Intentionally empty: telemetry is disabled (reinforced by Edit 3, §2).
+5 -2
View File
@@ -1,8 +1,11 @@
// docs: https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6
SWIFT_VERSION = 5.8
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES
MACOSX_DEPLOYMENT_TARGET = 10.13
SWIFT_TREAT_WARNINGS_AS_ERRORS = NO // replaced by the explicit flags below so the deprecation group can be downgraded after them
// Raising the unit-test target to 12.0 turns uses of APIs deprecated in macOS 11/12 (kept for the 10.13 app) into warnings; only that
// group is downgraded here (after -warnings-as-errors, which would otherwise win); every other warning stays an error.
OTHER_SWIFT_FLAGS = $(inherited) -warnings-as-errors -Wwarning DeprecatedDeclaration
MACOSX_DEPLOYMENT_TARGET = 12.0 // unit-tests only: Xcode 27 supports 12.0+; the shipping app keeps 10.13 in base.xcconfig
CODE_SIGNING_REQUIRED = NO // no need for codesign
CODE_SIGNING_ALLOWED = NO // no need for codesign
ONLY_ACTIVE_ARCH = YES // only need to run on the host machine
+1 -1
View File
@@ -10,7 +10,7 @@
## 1. What this project is, and the licensing position
**alt-tab-free** ([github.com/KofTwentyTwo/alt-tab-free](https://github.com/KofTwentyTwo/alt-tab-free)) is a re-host of the popular open-source macOS window switcher **AltTab** ([github.com/lwouis/alt-tab-macos](https://github.com/lwouis/alt-tab-macos)). It is a pure Swift 5.8 AppKit application — no SwiftUI, no Interface Builder (per [`AGENTS.md`](../AGENTS.md)).
**alt-tab-free** ([github.com/KofTwentyTwo/CommandTabFree](https://github.com/KofTwentyTwo/CommandTabFree)) is a re-host of the popular open-source macOS window switcher **AltTab** ([github.com/lwouis/alt-tab-macos](https://github.com/lwouis/alt-tab-macos)). It is a pure Swift 5.8 AppKit application — no SwiftUI, no Interface Builder (per [`AGENTS.md`](../AGENTS.md)).
### It is not a diverged fork — it is upstream + a paywall commit
+98
View File
@@ -0,0 +1,98 @@
# CI/CD readiness — CommandTabFree (fork of lwouis/alt-tab-macos)
**Date:** 2026-06-16 **Branch:** `master` **Repo:** `KofTwentyTwo/CommandTabFree` (public)
**Status:** publishing pipeline + Sparkle auto-update **LIVE** — signed + notarized **v100.2.0** released (Developer ID `James Maes (2X834TJ5MA)`, `spctl`-accepted + stapled), feed served on GitHub Pages (`koftwentytwo.github.io/CommandTabFree/appcast.xml`). The release apparatus (cert-import → sign-build → notarize → appcast → semantic-release → gh-release) is gated on a `will-release` signal (empty `VERSION.txt` ⇒ no release), so docs/ci pushes build+test without cutting a release or needing `[skip ci]`.
**Authoritative design:** `docs/PLAN-maintained-fork.md` §4; owner checklist `docs/EXECUTION-STATUS.md` §3.4/§3.5.
This is the state after the CI-fix pass. The pass applied only **safe, owner-secret-independent**
adaptations so that **push CI is GREEN today** on the not-yet-signing-provisioned fork, while the
release tail **flips on automatically** the moment the owner adds the Apple/Sparkle secrets. No secret
values were invented; no DOMAIN fabricated; notarization is not enabled without a cert.
---
## What changed in this pass
| File | Change |
|---|---|
| `.github/workflows/ci_cd.yml` | Added a `release-gate` step that emits `signing-ready` from secret presence (`APPLE_P12_CERTIFICATE`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID`/`SPARKLE_ED_PRIVATE_KEY`). Gated codesign/notarize/appcast/semantic-release/extract-changelog/gh-release/sync-state-advance behind `push && signing-ready=='true'`. Removed the `update_readme_and_website.sh` step (queried the **upstream** lwouis API — semantic mismatch, cosmetic). |
| `scripts/update_appcast.sh` | `sign_update -s $KEY` (DEPRECATED; per `--help` "no longer supported for newly generated keys" — would reject the fork's fresh EdDSA key) → `printf '%s' "$SPARKLE_ED_PRIVATE_KEY" \| sign_update --ed-key-file -`. No `-p` (default output is the `sparkle:edSignature/length` attr pair the enclosure needs; verified). Key now off the argv. |
| `commitlint.config.js` | Added `defaultIgnores: true` + `ignores` predicates (drop `chore(sync): merge upstream …`, `Merge …`, `Revert …`) so a sync merge's dragged-in upstream messages don't fail commitlint and block the release job (PLAN §4.3). |
| `scripts/update_readme_and_website.sh` | **Removed** — hardcoded `lwouis/alt-tab-macos` API + `lwouis` contributor graph; cosmetic SVG stats; no live caller after the workflow edit. |
| `scripts/update_website.sh` | **Removed** — dispatched to upstream `lwouis/alt-tab-website`; already had no live caller. |
| `scripts/upload_symbols_to_appcenter.sh` | **Removed** — hardcoded upstream AppCenter org; telemetry is off (PLAN §3.4); no live caller. |
All three workflow YAMLs validate (`yaml.safe_load` OK); `commitlint.config.js` loads in node.
---
## (A) WORKING / green NOW, without any owner secret
These run on every push and PR and need no Apple/Sparkle/DOMAIN/bot secret. They are what makes push
CI GREEN today (vs. the prior all-14-runs-RED state — note that RED was a *missing depaywall marker*,
already fixed on this branch; this pass ensures it does not go RED again on missing secrets).
| Capability | Where | Confidence |
|---|---|---|
| **Guard A** anti-relock unit tests (`run_tests.sh`, Test scheme) | `ci_cd.yml` push+PR; `guard.yml` PR (macos-15) | High — verified GREEN in EXECUTION-STATUS §2.2 (493 exec, 16 skipped, 0 fail) |
| **Guard B** de-paywall marker + `if false` wrap grep | `ci_cd.yml` first push step + PR; `guard.yml` PR | High — marker present at `LicenseManager.swift:177` |
| **Guard C** unresolved-conflict-marker grep | `ci_cd.yml` first push step + PR; `guard.yml` PR | High |
| **Compile the full tree** (`build_app.sh`, **ad-hoc**) | `ci_cd.yml` push | Verified GREEN on CI (run 27634828800). NOTE: the bare `build_app.sh` inherits upstream's Developer ID from `config/release.xcconfig:5` and fails with exit 65 on CI; the build step now runs ad-hoc (`CODE_SIGN_IDENTITY=-`) when `release-gate` is not signing-ready, and signed otherwise. `build_app.sh` forwards xcodebuild overrides via `"$@"`. |
| **commitlint** (push range + PR base..head, with `ignores`) | `ci_cd.yml` | Med-High — `ignores` should absorb upstream merge msgs; confirm on FIRST real sync PR |
| **generated-files-up-to-date** check | `ci_cd.yml` push+PR | High |
| **semantic-release dry-run** version compute (`determine_next_version.sh`) | `ci_cd.yml` push | High — dry-run, no push, no secret |
| **Release tail SKIPS cleanly** when secrets absent (warning, not failure) | `ci_cd.yml` `release-gate` | High — `signing-ready=false` path emits `::warning::` and skips |
> Note: full GREEN on the GitHub-hosted **macos-15** runner with **Xcode 26.0.1** is now CONFIRMED by a
> real run (`27634828800`, commit `6070f0cb`): gate + `run_tests.sh` + ad-hoc `build_app.sh` all pass and
> the release tail skips cleanly on absent secrets.
## (B) READY — flips ON automatically when the owner adds secret X
No further code change needed; the gate enables these the moment the secret(s) exist in the `production`
Environment.
| Capability | Enabling secret(s) | Gated step(s) |
|---|---|---|
| Code-sign keychain import | `APPLE_P12_CERTIFICATE` | `setup_ci_master.sh` |
| Notarize + staple | `APPLE_ID` + `APPLE_PASSWORD` + `APPLE_TEAM_ID` | `package_and_notarize_release.sh` |
| Sparkle appcast sign + publish to gh-pages | `SPARKLE_ED_PRIVATE_KEY` (+ a real `DOMAIN`/feed host) | `update_appcast.sh` (now `--ed-key-file -`) |
| Tag push + GitHub Release | ambient `GITHUB_TOKEN` (auto) — only gated alongside the above so a release isn't cut unsigned | `npx semantic-release`, `softprops/action-gh-release` (the release-tail `.fork-sync-state` advance step was **REMOVED** 2026-06-16 — it clobbered the upstream-sync cursor with the fork release tag; the cursor must be advanced by the sync-merge flow instead — see `ci_cd.yml`) |
| Sync PR fires `guard.yml`/`ci_cd.yml` PR checks | `SYNC_BOT_TOKEN` (GitHub App token or fork PAT — NOT default `GITHUB_TOKEN`) | `upstream_sync.yml` (fail-closed if missing) |
> The release tail is **all-or-nothing**: it requires the full Apple set **and** the Sparkle key
> (`signing-ready` is the AND of all five). This is deliberate — cutting a tag/Release without a signed,
> notarized, Sparkle-signed artifact would publish an un-updatable or Gatekeeper-blocked build and burn a
> version number. If you want to test the build path before the Apple cert exists, push runs build + Guard
> A and stops cleanly.
## (C) OWNER-BLOCKED — exact action required (cannot be done by agents)
| # | Blocker | Exact owner action |
|---|---|---|
| 1 | Apple Developer ID cert | Obtain a **Developer ID Application** cert for the fork; base64 it into secret `APPLE_P12_CERTIFICATE`; set `CODE_SIGN_IDENTITY` in `config/local.xcconfig` (currently commented). Re-sign the vendored `vendor/Sparkle/Helpers/*` under the SAME ID or notarization rejects (PLAN §3.3). |
| 2 | Notarization creds | Set secrets `APPLE_ID`, `APPLE_PASSWORD` (app-specific password), `APPLE_TEAM_ID` in the `production` Environment. |
| 3 | Sparkle private key | Store `SPARKLE_ED_PRIVATE_KEY` (already generated 2026-06-15) in the `production` Environment. Confirm the `Info.plist` `SUPublicEDKey` is the real public half (currently may be a placeholder). |
| 4 | `production` Environment | Create the GitHub Environment named **`production`** (matches `ci_cd.yml: environment: production`) with **no required-reviewer protection** that would stall releases; attach secrets #1–#3. |
| 5 | Real DOMAIN / appcast host | Replace `DOMAIN`/`API_DOMAIN` placeholders (`fork.invalid`) in `config/local.xcconfig` with fork-owned routable hosts; stand up `https://<DOMAIN>/appcast.xml` (gh-pages); `git rm appcast.xml` + gitignore it (PLAN §3.2). Until done, auto-update has no feed even with the key. |
| 6 | `SYNC_BOT_TOKEN` | Provision a GitHub App installation token (preferred) or fork PAT with contents + pull-requests write. Default `GITHUB_TOKEN` does NOT re-trigger PR checks → guards never run on the sync PR (`upstream_sync.yml` fails closed without it). |
| 7 | PR labels | `gh label create sync` / `conflict` / `chokepoint-refresh` (`upstream_sync.yml` fails closed if any is missing). |
| 8 | External scheduler | Hit `repository_dispatch` (type `upstream-sync`) from an external cron (immune to GitHub's 60-day in-repo `schedule:` auto-disable); add the two-trigger liveness alert (PLAN §4.2). |
| 9 | Xcode re-pin discipline | When an upstream sync bumps `ci_cd.yml`'s pinned Xcode, re-pin `guard.yml`'s `Xcode_26.0.1` line to match (EXECUTION-STATUS §3.5). |
| 10 | Bootstrap acceptance tests | (a) sync PR through the bot shows guard checks; (b) a deliberately-relocked branch goes RED on `guard.yml`; (c) fork build N→N+1 auto-updates via Sparkle (PLAN §4.2/§4.4/§6.1). |
---
## Remaining risks (need a real CI run to confirm)
- **macos-15 + Xcode_26.0.1 availability** on GitHub-hosted runners is assumed, not verified here.
- **commitlint `ignores`** is verified to load and match our synthetic subject, but the exact set of
*upstream* non-conforming messages can't be enumerated until a real `chore(sync)` merge — re-verify on
the FIRST sync PR (PLAN §4.3 says confirm on first sync).
- **`semantic-release` v15 / commitlint v8 on node-16**: kept per PLAN §4.3 (engines mismatch only warns);
unverified end-to-end on the runner since the release tail can't run without secrets.
- **`update_appcast.sh` gh-pages publish path** (orphan-branch seed, append-before-`</channel>`) is logic
the agents can't exercise without `GITHUB_TOKEN` + a real repo; verify on the first signed release.
- **`set -x` in `update_appcast.sh`** will print the `printf '%s' "$SPARKLE_ED_PRIVATE_KEY"` command in
the (private-repo) log trace; the key is no longer on the argv/process list, but consider `set +x` around
the sign line if the repo ever goes from private logs to public. (Not changed in this pass — out of scope.)
+7 -12
View File
@@ -188,23 +188,18 @@ per-component owner actions gathered from the scaffolding agents merged in.
### 3.1 Brand / identity / trademark (BLOCKING for a clean publish)
- [ ] **Pick a trademark-safe fork name** (avoid "AltTab"/"alt-tab" confusion) and run the PLAN §3.2
brand sweep across the tree, then **regenerate l10n** (`genstrings` per the §3.2 procedure) for
every file the sweep moved/renamed.
- [ ] **Set the REAL `PRODUCT_NAME`** in `config/local.xcconfig` if `AltTabFree` is not final, AND set
`ci_cd.yml` `APP_NAME` equal to it (currently placeholder `AltTabFree`).
- [ ] **Set the REAL fork bundle id in `config/local.xcconfig` and FREEZE it once** (placeholder:
`io.github.koftwentytwo.alt-tab-free`). Changing it later orphans every user's keychain +
UserDefaults state (LicenseManager derives the suite names from `App.bundleIdentifier`). Decide
PLAN §3.1: (a) fresh id, or (b) reuse-existing-id + migration. **Freeze the bundle id.**
- [ ] **Replace placeholder identity in `README.md` AND `NOTICE.md`** — final product name, final
bundle id, and the fork GitHub URL (placeholder `https://github.com/koftwentytwo/alt-tab-free`).
- [x] **Fork name + brand sweep + l10n (DONE 2026-06-15)** — name is **CommandTabFree** (trademark-distinct);
brand sweep done; l10n rebranded across base/en + all ~20 language tables, upstream attribution kept.
- [x] **`PRODUCT_NAME = CommandTabFree` (DONE)** in `config/local.xcconfig`. Verify `ci_cd.yml` `APP_NAME`
matches when CI is wired.
- [x] **Fork bundle id FROZEN (DONE)** — `com.koftwentytwo.commandtabfree` (option (a) fresh id) in `config/local.xcconfig`.
- [x] **`README.md` + `NOTICE.md` (DONE)** — final product name, real fork GitHub URL; README now includes the brew install.
- [ ] **At release time, set `NOTICE.md`'s upstream version + modification date** to the actual sync
point (currently `v11.3.0` / `2026-06-10`). Keep `NOTICE.md` current per PLAN §6.2 on every future
sync that changes the fork's modification surface. **Note:** `NOTICE.md` lists
"App.handleCustomUrl (activate disabled)" as a change — the §2 Edit-2 neutralization HAS landed
(`App.swift:474`), so that line is accurate; if you ever opt NOT to neutralize it, remove that line.
- [ ] **Replace the app icon** (do not ship lwouis's mark).
- [x] **App icon DONE (owner-approved 2026-06-15)** — the ⌘⇥ glyph (app + menu bar) is the chosen mark, not lwouis's mark. No replacement planned.
- [ ] **Add `config/local.xcconfig` and `package.json` to the PLAN §6.2 "keep ours" recurring-conflict
set** so a future merge can't re-introduce upstream identity or the MIT license string.
+20 -6
View File
@@ -1,15 +1,31 @@
# SESSION-STATE — CommandTabFree (de-paywalled AltTab fork)
_Updated 2026-06-15. `master` now carries the fork (merged + pushed). Public pre-release **v100.0.3** live; Homebrew tap live._
_Updated 2026-06-16. `master` carries the fork; the **full CI/CD publishing pipeline + auto-update are LIVE** — signed + notarized **v100.2.0** released (Gatekeeper-accepted), Sparkle feed served on GitHub Pages. Homebrew tap live._
## ✅ SIGNED + NOTARIZED RELEASE LIVE
The complete release pipeline ran green end-to-end (run `27643432063`, commit `2f9fac17`): cert import → **signed** `build_app.sh` (Developer ID **`James Maes (2X834TJ5MA)`**) → notarize + staple → appcast→gh-pages → semantic-release → GitHub Release. **[v100.1.0](https://github.com/KofTwentyTwo/CommandTabFree/releases/tag/v100.1.0)** is published; the downloaded asset verifies `spctl` = "accepted, source=Notarized Developer ID" and `stapler validate` passes (offline-clean, no quarantine prompt). Non-release pushes stay green via the ad-hoc `build_app.sh` path (run `27634828800`).
**How signing is wired:** `config/local.xcconfig` sets `CODE_SIGN_IDENTITY` + `CODE_SIGN_STYLE = Manual` + `TEAM_ID 2X834TJ5MA`; the vendored Sparkle helpers (`vendor/Sparkle/Helpers/*`) are committed **re-signed** under the same Developer ID; `setup_ci_master.sh` imports the cert with an optional `APPLE_P12_PASSWORD`. The release secrets (`APPLE_P12_CERTIFICATE`/`APPLE_P12_PASSWORD`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID` + `SPARKLE_ED_PRIVATE_KEY`) are set in the `production` env. Getting here also required two CI fixes earlier this session: `d1e1b296` (regenerate base `Localizable.strings` to clear the genstrings gate) and `6070f0cb` (ad-hoc build until the cert existed).
**Auto-update is LIVE (shipped in v100.2.0):** `DOMAIN = koftwentytwo.github.io/CommandTabFree`, `SUEnableAutomaticChecks = true`, the in-tree `appcast.xml` skeleton removed + gitignored. GitHub Pages serves `https://koftwentytwo.github.io/CommandTabFree/appcast.xml` (gh-pages branch, published by `update_appcast.sh`); the feed carries v100.1.0 + v100.2.0 enclosures. A kof22.com custom domain can be added later (GitHub redirects the github.io URL — no broken builds). **Homebrew cask** bumped to v100.2.0 + quarantine-strip postflight dropped (`KofTwentyTwo/homebrew-tap`). **Release-gate decoupled:** the sign/notarize/release apparatus is gated on a `will-release` signal (empty `VERSION.txt` ⇒ no release), so docs/ci pushes build+test without cutting a release or needing `[skip ci]` (ci_cd.yml).
**Remaining (owner-gated, NOT automatable):**
- **Upstream-sync**: provision `SYNC_BOT_TOKEN` + an external `repository_dispatch` cron; re-wire the `.fork-sync-state` cursor advance into the sync-merge flow (recording the UPSTREAM tag). `.fork-sync-state` = `v11.3.0`; the 3 sync labels exist.
- **(Optional)** kof22.com custom domain for the appcast (one DNS CNAME → `koftwentytwo.github.io`).
**Repo facts:** GitHub = `KofTwentyTwo/CommandTabFree` (local dir `~/Git.Local/kof22/alt-tab-free`, SSH remote). Latest release **v100.2.0** (signed + notarized + auto-updating). ⌘⇥ icon FINAL. Owner-gated checklist: `docs/TODO.md`.
## What this is
`KofTwentyTwo/alt-tab-free` is a fork of **AltTab** (`lwouis/alt-tab-macos`, GPL-3.0) that neutralizes the v11 Pro paywall so all features are free, rebranded **CommandTabFree**. Strategy: thin, merge-stable patch tracking upstream forever (`docs/PLAN-maintained-fork.md`).
`KofTwentyTwo/CommandTabFree` is a fork of **AltTab** (`lwouis/alt-tab-macos`, GPL-3.0) that neutralizes the v11 Pro paywall so all features are free, rebranded **CommandTabFree**. Strategy: thin, merge-stable patch tracking upstream forever (`docs/PLAN-maintained-fork.md`).
## ✅ DONE
- De-paywall + anti-relock tests + frozen identity (`com.koftwentytwo.commandtabfree`) + brand sweep + placeholder icon — all on **`master`** (was `depaywall-free`, fast-forward merged, HEAD `c7d2d968`, pushed to origin; source is public for GPL §6).
- De-paywall + anti-relock tests + frozen identity (`com.koftwentytwo.commandtabfree`) + brand sweep + ⌘⇥ icon (final) — all on **`master`** (was `depaywall-free`, fast-forward merged, HEAD `c7d2d968`, pushed to origin; source is public for GPL §6).
- **Sparkle EdDSA keypair generated.** Public key in `Info.plist` (`SUPublicEDKey`); private key in 1Password item **"CommandTabFree-Sparkle-EdDSA"** + GitHub Actions secret `SPARKLE_ED_PRIVATE_KEY` (env `production`); keychain account `commandtabfree`. `SUEnableAutomaticChecks=false` until a real appcast host exists. This fixed the launch-time "updater failed to start" alert.
- **Public release v100.0.3** (pre-release, **unsigned / not notarized**, universal x86_64+arm64) — ⌘⇥ menu-bar icon, redesigned About (KofTwentyTwo identity + AltTab attribution links), and a **full l10n rebrand** (product name → CommandTabFree across base/en + all ~20 translated languages; upstream attribution kept). Earlier v100.0.0/v100.0.1/v100.0.2 superseded.
- **Homebrew tap live:** `brew install --cask koftwentytwo/tap/commandtabfree` (`KofTwentyTwo/homebrew-tap` → `Casks/commandtabfree.rb`). Cask strips the quarantine (postflight `xattr`) for the unsigned app.
- **Homebrew tap live:** `brew install --cask koftwentytwo/tap/commandtabfree` (`KofTwentyTwo/homebrew-tap` → `Casks/commandtabfree.rb`). Cask strips the quarantine (postflight `xattr`) — was needed for the unsigned v100.0.x; now that v100.1.0 is notarized, drop that postflight and bump the cask.
- **CI/CD wired** (GitHub Actions), **fully green on push** (run `27634828800`, commit `6070f0cb`). Passing: Guard A/B/C, commitlint, generated-files gate, version compute, `run_tests.sh`, ad-hoc `build_app.sh`. The sign/notarize/appcast/semantic-release/gh-release tail is gated on a `release-gate` step that **auto-activates** when `APPLE_*` secrets are added — secrets were added 2026-06-16 and the **full tail is now proven** (signed+notarized **v100.1.0**, run `27643432063`). Fixed this session (2026-06-16): regenerated base `Localizable.strings` to clear the genstrings gate; build ad-hoc until the owner provisions the Apple cert (was inheriting upstream's Developer ID → exit 65). Earlier this session: Guard B grep now matches the shipped `guard false else { return .pro }` patch (was checking the PLAN's never-shipped `if false`/`alt-tab-free [depaywall]` form); `update_appcast.sh` → `--ed-key-file`; commitlint sync-merge `ignores`; dropped upstream-only scripts (AppCenter/website); commit headers must be ≤72 chars. Full status: `docs/CICD-STATUS.md`.
- **Local daily driver installed:** `/Applications/CommandTabFree.app` = **Release** build (no QA menu) signed with the `Local Self-Signed` cert (stable signature → no TCC re-grant loop). Remaining: user grants Accessibility + Screen Recording on first launch.
### Build recipes (verified this session)
@@ -21,8 +37,6 @@ _Updated 2026-06-15. `master` now carries the fork (merged + pushed). Public pre
- **Apple Developer ID** (long pole): set `CODE_SIGN_IDENTITY` in `config/local.xcconfig`, re-sign Sparkle helpers, notarize → re-release signed/notarized, then drop the cask's quarantine postflight.
- **Real DOMAIN/appcast** (kof22.com ready): replace `fork.invalid` in `local.xcconfig`, serve `/appcast.xml`, flip `SUEnableAutomaticChecks` back on.
- **Wire CI:** `update_appcast.sh` uses the deprecated `sign_update -s` — switch to `--ed-key-file` (or `--account commandtabfree`) for the new key; create `sync`/`conflict`/`chokepoint-refresh` labels; external cron for `upstream_sync.yml`.
- **Real icon artwork** (replace placeholder).
## Gotchas
- Bundle id FROZEN (`com.koftwentytwo.commandtabfree`); keychain/UserDefaults suites derive from it.
- Don't `git revert 9147a4a8` (mixes paywall with a 1869-file reorg).
+23
View File
@@ -0,0 +1,23 @@
# TODO — CommandTabFree
_Updated 2026-06-16. The **publishing pipeline + auto-update are LIVE** — **v100.2.0** released (signed, notarized, Gatekeeper-accepted, auto-updating). The only substantial remaining work is upstream-sync automation. Full detail: `docs/CICD-STATUS.md`, `docs/EXECUTION-STATUS.md` §3._
## ✅ Done this session (2026-06-16)
- [x] **Signed + notarized releases** — Developer ID `James Maes (2X834TJ5MA)`; cert + creds in `production` secrets; `CODE_SIGN_IDENTITY`/Manual/`TEAM_ID` in `config/local.xcconfig`; vendored Sparkle helpers re-signed. Full tail proven (runs `27643432063` v100.1.0, `27647918163` v100.2.0).
- [x] **Auto-update** — `DOMAIN = koftwentytwo.github.io/CommandTabFree`, `SUEnableAutomaticChecks = true`, in-tree `appcast.xml` removed + gitignored. GitHub Pages serves the gh-pages feed; v100.2.0 ships auto-updating.
- [x] **Homebrew cask** — bumped to v100.2.0 + quarantine-strip postflight dropped (`KofTwentyTwo/homebrew-tap`).
- [x] **Release-gate decouple** — sign/notarize/release apparatus gated on a `will-release` signal (empty `VERSION.txt` ⇒ no release); non-release pushes build+test without cutting a release or needing `[skip ci]`.
- [x] genstrings gate fixed (`d1e1b296`); ad-hoc-until-cert build (`6070f0cb`); `.fork-sync-state` reset to `v11.3.0` + release tail no longer clobbers it; 3 sync labels created.
## Remaining — Upstream-sync automation (owner-gated)
- [ ] **`SYNC_BOT_TOKEN`** — GitHub App installation token (or fork PAT with contents + pull-requests write). Default `GITHUB_TOKEN` does NOT re-trigger PR checks, so guards never run on the sync PR.
- [ ] **External cron** — hit `repository_dispatch` (type `upstream-sync`) from an external scheduler (immune to GitHub's 60-day in-repo `schedule:` auto-disable).
- [ ] **Re-wire the `.fork-sync-state` cursor advance** into the upstream-sync merge flow so it records the synced **UPSTREAM** tag (not the fork release tag) — `upstream_sync.yml` (PLAN §4.2).
## Optional
- [ ] kof22.com custom domain for the appcast (one DNS CNAME → `koftwentytwo.github.io`; GitHub redirects the github.io URL, so existing builds keep updating).
## Bootstrap acceptance tests (after upstream-sync is wired)
- [ ] Sync PR through the bot shows guard checks running.
- [ ] A deliberately-relocked branch goes RED on `guard.yml`.
- [x] Fork build N→N+1 auto-updates via Sparkle — infra live (feed served + v100.2.0 auto-update-enabled); end-to-end N→N+1 confirmed on the next release.
+1 -1
View File
@@ -232,7 +232,7 @@ update from the fork's own releases, and attempt to notarize/sign under the upst
| 6 | **Sparkle private EdDSA key** | `ci_cd.yml:13`; `update_appcast.sh:9` | `$SPARKLE_ED_PRIVATE_KEY` | **C** — fork's new private key. Must match #5 or updates won't verify. |
| 7 | **Appcast feed domain** | `config/base.xcconfig:20` (`DOMAIN`) → `Endpoints.swift:6-7` | `alt-tab.app` → `https://alt-tab.app/appcast.xml` | **C** — fork's domain, OR repoint feed at fork's GitHub-hosted appcast. |
| 8 | **API domain (license + feedback)** | `config/base.xcconfig:21` (`API_DOMAIN`) → `Endpoints.swift:5,11-12` | `alt-tab.app/api` → `https://alt-tab.app/api/v1/license`, `.../v1/feedback` | **R** — license API is paywall; remove with `src/pro`. Feedback endpoint must be repointed or removed. |
| 9 | **Appcast download URL** | `update_appcast.sh:18` | `https://github.com/lwouis/alt-tab-macos/releases/download/v<v>/...` | **C** — fork's GitHub releases URL (`KofTwentyTwo/alt-tab-free`). |
| 9 | **Appcast download URL** | `update_appcast.sh:18` | `https://github.com/lwouis/alt-tab-macos/releases/download/v<v>/...` | **C** — fork's GitHub releases URL (`KofTwentyTwo/CommandTabFree`). |
| 10 | **Appcast release-notes link** | `update_appcast.sh:16`; existing items in `appcast.xml` | `https://alt-tab.app/changelog-bare` | **C/R** — fork URL or drop the link. |
| 11 | **License tier cookie** | `src/pro/license/LicenseCookie.swift` | cookie on `alt-tab.app` | **R** — remove with paywall. |
| 12 | **Website dispatch repo + token** | `update_website.sh:5`; `ci_cd.yml:69` | `lwouis/alt-tab-website` + `$WEBSITE_DISPATCH_TOKEN` | **R/C** — fork has no upstream website; remove this CI step or repoint. |
+3 -3
View File
@@ -30,7 +30,7 @@ modified. All paths are relative to the repo root unless noted.
`semantic-release-bot`; there are **no fork-specific commits**. The entire paywall was
introduced **upstream** by lwouis in a single commit, `9147a4a8 "feat: introducing
alt-tab pro!"` (v11.0.0, 2026-04-05). The "alt-tab-free" repo has only re-hosted the
code under `KofTwentyTwo/alt-tab-free` — **no code-level rebrand has happened yet**:
code under `KofTwentyTwo/CommandTabFree` — **no code-level rebrand has happened yet**:
bundle id, app name, domain, GitHub URL, and SUPublicEDKey are all still the upstream
`com.lwouis.alt-tab-macos` / `alt-tab.app` / `lwouis/alt-tab-macos` values.
@@ -242,7 +242,7 @@ git log --oneline -- src/pro # only 3 commits ever touch src/pro
git log --oneline --diff-filter=A -- src/pro | tail -5 # src/pro first appears in 9147a4a8
git log -40 --format='%an' | sort | uniq -c # 34 lwouis, 6 semantic-release-bot
git rev-list --count HEAD # 1850 commits total
git remote -v # origin = github.com:KofTwentyTwo/alt-tab-free.git
git remote -v # origin = github.com:KofTwentyTwo/CommandTabFree.git
```
Findings:
@@ -257,7 +257,7 @@ Findings:
fixes after the pro launch (`f3fe535a`, `cddc89dd`, `e0b1778d`, etc.) — is `lwouis`.
There are **zero commits by KofTwentyTwo / the current owner**. The "alt-tab-free" repo
is a mirror of upstream `lwouis/alt-tab-macos` re-hosted under a new origin
(`git@github.com:KofTwentyTwo/alt-tab-free.git`), with the codebase otherwise unchanged.
(`git@github.com:KofTwentyTwo/CommandTabFree.git`), with the codebase otherwise unchanged.
The README still markets it as "AltTab Pro" and links `https://alt-tab.app/`
(`README.md:1`).
+6
View File
@@ -11,6 +11,12 @@ This project has minimal dependency on Xcode-only features (e.g. InterfaceBuilde
* `scripts/codesign/setup_local.sh` to generate a local self-signed certificate, to avoid having to re-check the `System Preferences > Security & Privacy` permissions on every build
* Either open `alt-tab-macos.xcodeproj` with Xcode, or use the CLI: `xcodebuild -project alt-tab-macos.xcodeproj -scheme Debug` to build the .app with the `Debug` build configuration
### Tests (run locally before merging)
The Gitea CI (`.gitea/workflows/build.yaml`) runs Guards B and C (the de-paywall markers and conflict markers), builds the `Release` scheme unsigned and compiles the `Test` scheme (`build-for-testing`), but does not run the tests: its macOS runner has no GUI session, so `xcodebuild test` cannot reach `testmanagerd`. Run them, including the anti-relock tests in `src/pro/license/LicenseManagerTests.swift` (Guard A, `docs/PLAN-maintained-fork.md` §4.4), locally before merging, especially an upstream sync:
* `scripts/run_tests.sh` (the `Test` scheme, `-configuration Release`)
## Mac development
Mac development ecosystem is pretty terrible in general. They keep piling on the tech stacks on top of each other, so you have C APIs, ObjC APIs, Swift APIs, Interface builder, Playgrounds, Swift UI, Mac Catalyst. All these are bridging with each other with a bunch of macros, SDKs glue, compiler flags, compatibility mode, XCode legacy build system, etc. For alt-tab, we are on Swift 5.0. Note that swift just recently started being stable, but overall any change of version breaks a lot of stuff. Swift itself is the mainstream language with the worst governance I’ve seen in modern times.
+22 -16
View File
@@ -1,3 +1,6 @@
/* %@ is the app name */
"%@ is a free, de-paywalled fork. All upstream work and credit belong to Louis Pontoise and AltTab’s contributors." = "%@ is a free, de-paywalled fork. All upstream work and credit belong to Louis Pontoise and AltTab’s contributors.";
/* No comment provided by engineer. */
"%@ now has a Pro tier" = "%@ now has a Pro tier";
@@ -22,13 +25,13 @@
/* No comment provided by engineer. */
"A debug profile (versions, settings, hardware) is attached to help diagnose the issue." = "A debug profile (versions, settings, hardware) is attached to help diagnose the issue.";
/* Attribution to the upstream project AltTab; do not translate the product names */
"A free fork of AltTab by Louis Pontoise — GPL-3.0" = "A free fork of AltTab by Louis Pontoise — GPL-3.0";
/* No comment provided by engineer. */
"A KofTwentyTwo project" = "A KofTwentyTwo project";
/* No comment provided by engineer. */
"A new version of CommandTabFree is available" = "A new version of CommandTabFree is available";
/* Menubar option. %@ is CommandTabFree */
/* Menubar option. %@ is AltTab */
"About %@" = "About %@";
/* No comment provided by engineer. */
@@ -86,16 +89,16 @@
"Alphabetical Order" = "Alphabetical Order";
/* No comment provided by engineer. */
"CommandTabFree is a free, open-source window switcher for macOS. Pro features are available with a 14-day free trial." = "CommandTabFree is a free, open-source window switcher for macOS. Pro features are available with a 14-day free trial.";
"AltTab is a free, open-source window switcher for macOS. Pro features are available with a 14-day free trial." = "AltTab is a free, open-source window switcher for macOS. Pro features are available with a 14-day free trial.";
/* No comment provided by engineer. */
"CommandTabFree Pro" = "CommandTabFree Pro";
"AltTab Pro" = "AltTab Pro";
/* No comment provided by engineer. */
"CommandTabFree Pro adds 4 features beyond the free switcher." = "CommandTabFree Pro adds 4 features beyond the free switcher.";
"AltTab Pro adds 4 features beyond the free switcher." = "AltTab Pro adds 4 features beyond the free switcher.";
/* No comment provided by engineer. */
"CommandTabFree Pro is still available" = "CommandTabFree Pro is still available";
"AltTab Pro is still available" = "AltTab Pro is still available";
/* No comment provided by engineer. */
"Always" = "Always";
@@ -172,6 +175,9 @@
/* No comment provided by engineer. */
"Automatic activation failed" = "Automatic activation failed";
/* Attribution to the upstream project AltTab; do not translate the product names */
"Based on AltTab by Louis Pontoise" = "Based on AltTab by Louis Pontoise";
/* No comment provided by engineer. */
"Brief description of the bug" = "Brief description of the bug";
@@ -344,7 +350,7 @@
"Gesture" = "Gesture";
/* No comment provided by engineer. */
"Get more from CommandTabFree with Pro" = "Get more from CommandTabFree with Pro";
"Get more from AltTab with Pro" = "Get more from AltTab with Pro";
/* Menubar option */
"Get Pro" = "Get Pro";
@@ -422,7 +428,7 @@
"License doesn't cover this version" = "License doesn't cover this version";
/* No comment provided by engineer. */
"License will remain valid and usable to activate CommandTabFree on any machine" = "License will remain valid and usable to activate CommandTabFree on any machine";
"License will remain valid and usable to activate AltTab on any machine" = "License will remain valid and usable to activate AltTab on any machine";
/* No comment provided by engineer. */
"Light" = "Light";
@@ -520,6 +526,9 @@
/* No comment provided by engineer. */
"Ordering & Grouping" = "Ordering & Grouping";
/* No comment provided by engineer. */
"Original source" = "Original source";
/* No comment provided by engineer. */
"Overridden in Shortcut:" = "Overridden in Shortcut:";
@@ -562,7 +571,7 @@
/* No comment provided by engineer. */
"Pro Lifetime license activated for %@" = "Pro Lifetime license activated for %@";
/* %@ is CommandTabFree */
/* %@ is AltTab */
"Quit %@" = "Quit %@";
/* No comment provided by engineer. */
@@ -874,9 +883,6 @@
/* No comment provided by engineer. */
"Visible Spaces" = "Visible Spaces";
/* No comment provided by engineer. */
"Website" = "Website";
/* No comment provided by engineer. */
"Welcome to %@" = "Welcome to %@";
@@ -920,7 +926,7 @@
"You can’t undo this action." = "You can’t undo this action.";
/* No comment provided by engineer. */
"You have 14 days to try all Pro features. After that, Pro features will step back and the core window switcher will keep working exactly as before.\n\nCommandTabFree stays free and open-source. Pro is an optional one-time purchase that funds continued development." = "You have 14 days to try all Pro features. After that, Pro features will step back and the core window switcher will keep working exactly as before.\n\nCommandTabFree stays free and open-source. Pro is an optional one-time purchase that funds continued development.";
"You have 14 days to try all Pro features. After that, Pro features will step back and the core window switcher will keep working exactly as before.\n\nAltTab stays free and open-source. Pro is an optional one-time purchase that funds continued development." = "You have 14 days to try all Pro features. After that, Pro features will step back and the core window switcher will keep working exactly as before.\n\nAltTab stays free and open-source. Pro is an optional one-time purchase that funds continued development.";
/* No comment provided by engineer. */
"You may need to disable some conflicting system gestures" = "You may need to disable some conflicting system gestures";
@@ -938,10 +944,10 @@
"You've been using %d Pro features.\nThey'll switch back to defaults on Day 15." = "You've been using %d Pro features.\nThey'll switch back to defaults on Day 15.";
/* No comment provided by engineer. */
"You've used CommandTabFree %@ times — %@ of those used Pro features." = "You've used CommandTabFree %1$@ times — %2$@ of those used Pro features.";
"You've used AltTab %@ times — %@ of those used Pro features." = "You've used AltTab %1$@ times — %2$@ of those used Pro features.";
/* No comment provided by engineer. */
"You've used CommandTabFree %@ times." = "You've used CommandTabFree %@ times.";
"You've used AltTab %@ times." = "You've used AltTab %@ times.";
/* No comment provided by engineer. */
"Your 14-day Pro trial just ended" = "Your 14-day Pro trial just ended";
+1 -1
View File
@@ -2,5 +2,5 @@
set -ex
set -o pipefail && xcodebuild -project alt-tab-macos.xcodeproj -scheme Release -derivedDataPath DerivedData | scripts/xcbeautify
set -o pipefail && xcodebuild -project alt-tab-macos.xcodeproj -scheme Release -derivedDataPath DerivedData "$@" | scripts/xcbeautify
file "$BUILD_DIR/$XCODE_BUILD_PATH/$APP_NAME.app/Contents/MacOS/$APP_NAME"
+1 -1
View File
@@ -7,4 +7,4 @@ certificateFile="codesign"
# Recreate the certificate from the secure environment variable
echo "$APPLE_P12_CERTIFICATE" | base64 --decode > $certificateFile.p12
scripts/codesign/import_certificate_into_new_keychain.sh "$certificateFile" ""
scripts/codesign/import_certificate_into_new_keychain.sh "$certificateFile" "${APPLE_P12_PASSWORD:-}"
+8 -1
View File
@@ -31,7 +31,14 @@ version="$(cat "$VERSION_FILE")"
date="$(date +'%a, %d %b %Y %H:%M:%S %z')"
minimumSystemVersion="$(awk -F ' = ' '/MACOSX_DEPLOYMENT_TARGET/ { print $2; }' < config/base.xcconfig)"
zipName="$APP_NAME-$version.zip"
edSignatureAndLength=$(vendor/Sparkle/bin/sign_update -s $SPARKLE_ED_PRIVATE_KEY "$XCODE_BUILD_PATH/$zipName")
# alt-tab-free [depaywall]: the `-s <private-key>` flag is DEPRECATED and, per `sign_update --help`,
# "no longer supported for newly generated keys" — and the fork's EdDSA keypair was newly generated
# (EXECUTION-STATUS §3.3, 2026-06-15), so `-s` would REJECT it. Pipe the secret to the documented
# `--ed-key-file -` (read key from stdin) form instead. Do NOT add `-p`: the default output is the
# `sparkle:edSignature="…" length="…"` attribute pair the <enclosure> needs verbatim, whereas `-p`
# prints only the bare signature and would break the enclosure. Reading from stdin also keeps the key
# off the argv (process listing), unlike the old `-s $KEY` form.
edSignatureAndLength=$(printf '%s' "$SPARKLE_ED_PRIVATE_KEY" | vendor/Sparkle/bin/sign_update --ed-key-file - "$XCODE_BUILD_PATH/$zipName")
# GPL §6(d) corresponding-source pointer for object code conveyed via Sparkle auto-update.
correspondingSource="https://github.com/$fork_repo/archive/refs/tags/v$version.tar.gz"
-82
View File
@@ -1,82 +0,0 @@
#!/usr/bin/env bash
set -exu
github_api_request() {
local url="$1"
curl -s \
-H "Accept: application/vnd.github+json" \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/lwouis/alt-tab-macos$url"
}
unicode_sort() {
python3 -c 'import sys, unicodedata; print("".join(sorted((line for line in sys.stdin if line.strip()), key=lambda s: unicodedata.normalize("NFKD", s.casefold()))), end="")'
}
github_contributors() {
github_api_request "/contributors" |
jq -r '.[]|("[" + .login + "](" + .html_url + ")")' |
sed -e '/semantic-release-bot/d' |
unicode_sort |
awk '{printf "%s%s", sep, $0; sep=", "} END{print ""}'
}
# Rewrite only the "Developed the app" section of docs/contributors.md, preserving the
# frozen "Localized the app" section below it.
update_developer_contributors() {
local file="docs/contributors.md"
{
echo "## [Developed the app](https://github.com/lwouis/alt-tab-macos/graphs/contributors)"
echo
github_contributors
echo
sed -n '/## Localized the app/,$p' "$file"
} > "$file.tmp" && mv "$file.tmp" "$file"
}
get_total_downloads() {
local downloads=0
local page=1
while true; do
local response
response=$(github_api_request "/releases?per_page=100&page=$page")
# Safely sum all download counts, even if assets are empty
local count
count=$(echo "$response" | jq '[.[]?.assets[]?.download_count // 0] | add // 0')
downloads=$((downloads + count))
# Stop if no more releases
if [[ $(echo "$response" | jq 'length') -lt 1 ]]; then
break
fi
page=$((page + 1))
done
echo "$downloads"
}
get_stars() {
github_api_request "" | jq '.stargazers_count'
}
format_number() {
local NUM=$1
if (( NUM >= 1000000 )); then
printf "%.1fM" "$(echo "$NUM/1000000" | bc -l)"
elif (( NUM >= 1000 )); then
printf "%.0fK" "$(echo "$NUM/1000" | bc -l)"
else
printf "%d" "$NUM"
fi
}
downloads=$(format_number "$(get_total_downloads)")
stars=$(format_number "$(get_stars)")
# Stats are baked into docs/readme/main.svg (one consolidated SVG that holds
# the hero, stats, CTAs, and screenshot). Each stat text element is preceded by
# an XML comment marker — anchor sed on the marker and replace whatever value
# follows up to the next `<` (i.e. the closing `</text>`).
sed -i "" -E "s|(<!--downloads-->)[^<]*|\1${downloads}|" "docs/readme/main.svg"
sed -i "" -E "s|(<!--stars-->)[^<]*|\1${stars}|" "docs/readme/main.svg"
update_developer_contributors
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env bash
set -exu
gh api repos/lwouis/alt-tab-website/dispatches -f event_type=update-website
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
set -exu
ownerName="alt-tab-macos"
appName="alt-tab-macos"
url="https://api.appcenter.ms/v0.1/apps/$ownerName/$appName"
version="$(cat "$VERSION_FILE")"
# Upload a single .dSYM bundle to AppCenter (zip, request slot, PUT to blob, mark committed).
# Called once per dSYM produced by the Release build:
# - AltTab.app.dSYM host binary + everything statically linked (AppCenter, AppCenterCrashes, ShortcutRecorder)
# - Sparkle.framework.dSYM Sparkle is the only .dynamic SPM product, so it has its own dSYM
# CrashReporter.xcframework ships without dSYMs (PLCrashReporter is binary-only); AppCenter has
# server-side symbols for it.
function upload_dsym() {
local symbolFile="$1"
ditto -c -k --keepParent "$symbolFile" "$symbolFile.zip"
local c1 symbol_upload_id upload_url
c1="$(curl -X POST "$url/symbol_uploads" \
-H "X-API-Token: $APPCENTER_TOKEN" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d "{ \"symbol_type\": \"Apple\", \"version\": \"$version\" }")"
symbol_upload_id="$(jq -r '.symbol_upload_id' <<<"$c1")"
upload_url="$(jq -r '.upload_url' <<<"$c1")"
curl -X PUT "$upload_url" \
-H "x-ms-blob-type: BlockBlob" \
--upload-file "$symbolFile.zip"
curl -X PATCH "$url/symbol_uploads/$symbol_upload_id" \
-H "X-API-Token: $APPCENTER_TOKEN" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{ "status": "committed" }'
}
cd "$XCODE_BUILD_PATH"
upload_dsym "$APP_NAME.app.dSYM"
upload_dsym "Sparkle.framework.dSYM"
+1 -1
View File
@@ -13,7 +13,7 @@ class App: AppCenterApplication {
static let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as! String
static let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
static let licence = Bundle.main.object(forInfoDictionaryKey: "NSHumanReadableCopyright") as! String
static let repository = "https://github.com/KofTwentyTwo/alt-tab-free"
static let repository = "https://github.com/KofTwentyTwo/CommandTabFree"
static let appIconReps = CGImage.allNamed("app.icns")
static func appIcon(for size: NSSize) -> CGImage {
@@ -291,7 +291,7 @@ class ExceptionEditorView: NSView {
}
private func resolveHeaderAsync(for bundleId: String) {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let info = AppDisplayInfo.resolve(bundleId: bundleId)
DispatchQueue.main.async { [weak self] in
guard let self, self.entry.bundleIdentifier == bundleId else { return }
@@ -319,7 +319,7 @@ class ExceptionsTab {
// BundleId changed (or first paint): show a synchronous placeholder, then resolve async.
row.setIcon(AppDisplayInfo.genericIcon, size: iconSize)
row.setContent(bundleId, summary)
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.global(qos: .userInitiated).async { [weak row] in
let info = AppDisplayInfo.resolve(bundleId: bundleId)
DispatchQueue.main.async { [weak row] in
guard let row else { return }
Binary file not shown.
Binary file not shown.