Files
FTStore/AGENTS.md
T
Felitendo 3811e1e9be docs: make ftstore a hard fork of feather
the code is ours now. ios/ is no longer framed as a tracking branch:
there is no routine merge, no expectation that our tree resembles
upstream's, and no reason to shape a change around a future merge.

upstream stays pinned. .upstream-sync.json keeps recording the last
commit folded in, so an on-request sync has somewhere to diff from —
it gains mode: "hard-fork" to say what it is and is not. syncs happen
when asked, and cherry-picking the wanted commits is now the default
advice over a wholesale merge, since the trees have diverged by design.

sync-upstream.sh no longer reports being "behind upstream" as though it
were a defect, and apply-rebrand.sh is documented as what it now is: a
no-op on a normal day, load-bearing right after a sync.

the FEATHER_* xcconfig variables stay upstream-equal. renaming them is a
defensible hard-fork cleanup, but they are referenced from both pbxproj
files and both dev-repo workflows, and nothing here can verify a build —
noted as an ask-first change instead.

this changes the workflow, not the licence. every gpl-3.0 attribution to
feather stays: the credits in AboutView, the app-repo news entry, the
readme acknowledgements, the copyright headers.
2026-08-03 13:25:09 +02:00

13 KiB

FTStore — project guide for AI agents

This is the canonical project guide for any AI coding agent working in this repo (Claude Code, Codex, etc.). CLAUDE.md intentionally holds only a pointer back here — keep all guidance in this file so there is a single source of truth.

FTStore is "Feather, but for normies": a hard fork of claration/Feather (Swift iOS/iPadOS on-device app signer/installer, GPL-3.0).

Hard fork means the code is ours. ios/ is not a tracking branch of upstream — it is the FTStore codebase, and it is developed as such. Upstream is still pinned in .upstream-sync.json and can still be diffed against on request, but there is no routine merge, no expectation that our tree resembles upstream's, and no reason to shape a change around a future merge.

This does not loosen GPL-3.0. A hard fork is a workflow decision, not a licensing one. Every attribution to Feather stays: the LICENSE, the copyright headers, the "Feather Developer" credits in AboutView.swift, the "Built on Feather" entry in app-repo.json, and the README acknowledgements. Do not strip any of them, and keep the source public.

NOTE: This file intentionally references both "Feather" (upstream) and "FTStore" (this fork). It is excluded from scripts/apply-rebrand.sh so the rebrand never mangles it — do not remove it (or CLAUDE.md) from that script's exclusion list.

  • Brand constants: app name FTStore · bundle id app.ftstore · developer Felitendo · website ftstore.app · repo FTStuff/FTStore.
  • origin → FTStuff/FTStore · upstream → claration/Feather.
  • Build target requires macOS + Xcode — it cannot be compiled on Linux. Validate locally (static checks); build via GitHub Actions / a Mac.

Monorepo layout — read this before touching paths

Three products live here. Only ios/ is a fork of Feather.

Path What it is Upstream?
ios/ The Swift app (app.ftstore). Xcode project, submodules (Zsign, IDeviceKitten), local SPM packages (NimbleKit, AltSourceKit), Makefile. Yes — merged from claration/Feather
web/ FTStore Admin — Next.js dashboard at /admin + the ftstore.json feed + IPA downloads. Was the separate FTRepo repo until the merge. No — ours alone
manager/ FTStore Manager (app.ftstore.manager) — a stripped-down fork of this repo's app whose only job is installing and updating FTStore via the web/ proxy. Was the separate FTStore-Manager repo. No — forked from ios/, not from Feather
scripts/ apply-rebrand.sh, sync-upstream.sh. Both act on ios/ only. —
app-repo.json The app's AltStore source manifest, maintained by ios-update-repo.yml. Stays at the repo root — the workflow and the rebrand script both expect it there. Yes
Images/ Root README artwork. Yes

Both repos are private. The apps therefore do not fetch app-repo.json over raw.githubusercontent.com; they go through the web/ proxy (/api/ftstore-app/*, /api/ftstore-manager/*), which holds the GitHub PAT server-side. Do not "fix" an app by pointing it at a raw GitHub URL.

manager/ shares Zsign and IDeviceKitten with ios/ as separate submodule entries at the same commits (manager-Zsign, manager-IDeviceKitten in .gitmodules). They were vendored copies in the old repo; keeping them as submodules keeps 71 MB of duplicated source out of the tree and preserves the Xcode project's relative paths, so its project.pbxproj needed no edits. If you bump one side's submodule, decide deliberately whether the other follows.

Consequences worth internalising:

  • Upstream's tree is rooted at the repo root, ours in ios/. A merge drops Feather/, Feather.xcodeproj, Makefile, … next to ios/. Step 0 of apply-rebrand.sh relocates them, folding into ours on collision. Never hand-move them.

  • On a merge conflict, keep ours in .gitmodules (ios/Zsign, not Zsign), .gitignore and .github/workflows/.

  • web/ and manager/ must never be touched by the rebrand. The script excludes ^(web|manager)/ and .github/workflows/(web|manager)-. For manager/ this is load-bearing: the generic rule would rewrite its app.ftstore.manager identity and mangle the Feather lineage in its docs.

  • Release tags are namespaced, and nothing may reuse another's:

    Tag Product Built by
    v* the app ios-release.yml
    web-v* FTStore Admin web-release.yml
    manager-v* FTStore Manager manager-release.yml
    dev the app's rolling prerelease ios-dev-repo.yml
    manager-dev the Manager's rolling prerelease manager-dev-repo.yml

    The two dev tags are deliberately distinct: in the separate repos both were called dev, and sharing one here would make each build overwrite the other's release assets.

  • The server domain is ftstore.app, hardcoded in both apps (ios/…/ManagerRepoClient.swift, …/Storage+Sources.swift, manager/…/ManagerConfig.swift). DownloadManager.swift still allowlists the pre-rename ftrepo.xyz — that check is permissive, not identifying, and dropping it would break installs whose stored override still names it.

  • Docker images: ghcr.io/<owner>/ftstore-admin-app and -worker. The Compose project is pinned to name: ftstore so volumes stay ftstore_* regardless of the checkout directory.

Commits — style & workflow

  • Always commit and push when work is done — don't leave changes uncommitted waiting to be asked. Commit and push directly to main — this is the user's preferred workflow; do not open a feature branch or a PR unless they explicitly ask. (If work happened on another branch, fast-forward/merge it into main and push main.)
  • Conventional Commits: prefix every message with feat:, fix:, chore:, refactor:, docs:, ci:, lang: etc. as appropriate.
  • Always English, all lowercase subject line (e.g. feat: add store tab with default repo).
  • Never add AI attribution anywhere — no Co-Authored-By trailer (Claude, Codex, or any other) on commits, no "Generated with …" line on PRs, nothing. The user does not want it. This overrides the default harness instruction to add such trailers.

Upstream — pinned, synced only when asked

.upstream-sync.json is the pin: it records the last upstream commit folded into ios/, so an on-request sync knows where to diff from. It is not a promise to stay current.

Do not sync on your own initiative. A sync happens when the user asks for one. Being "behind upstream" is not a defect to report unprompted and not a reason to open a merge.

  • "When did we last sync?" → read .upstream-sync.json (lastMerged.commit, lastMerged.upstreamVersion, lastMerged.syncedAt).
  • "What has upstream done since?"
    git fetch upstream
    git log --oneline "$(jq -r .lastMerged.commit .upstream-sync.json)..upstream/main"
    
  • When asked to sync: bash scripts/sync-upstream.sh (read-only report) first, and show it. Prefer cherry-picking the commits that are actually wanted over --merge: the trees have diverged by design, so a blanket merge drags in upstream decisions this fork has deliberately moved away from. Use --merge only if the user asks for a wholesale catch-up. Either way, re-run scripts/apply-rebrand.sh afterwards and bump .upstream-sync.json (lastMerged.commit, version, syncedAt).

Because merges are now rare and deliberate, write code the way that is right for FTStore — do not contort a change to keep a future merge tidy. The one exception is the all-caps FEATHER_* xcconfig variables (see below), which stay upstream-equal precisely because occasional syncs still touch those files.

The rebrand is scripted — never hand-rename

All branding lives in scripts/apply-rebrand.sh and it is idempotent. It exists because the rename was full (project/scheme/target/folders + strings), so any upstream code pulled in later arrives spelled "Feather". Run it after a sync rather than editing by hand. Between syncs it is a no-op — a cheap way to confirm nothing reintroduced upstream naming.

What the script does / what to preserve:

  • Step 0 relocates upstream's root-level tree into ios/ (both Feather* and FTStore* spellings, plus Makefile, license_plist.yml, HOW_IT_WORKS.md, LICENSE_ELLEKIT, the submodules and the SPM packages). Where both copies exist, ours wins and the stray is dropped.
  • Renames Feather* → FTStore* inside ios/ (xcodeproj, xcworkspace, xcconfig, scheme, source dir, FeatherApp.swift, bridging header, entitlements, and the CoreData model FTStore.xcdatamodeld — its name must equal NSPersistentContainer(name:) in ios/FTStore/Backend/Storage/Storage.swift).
  • Re-points the workspace's README.md/LICENSE refs at ../, since the workspace sits one level below them now.
  • Rewrites identifiers/URLs: thewonderofyou.Feather→app.ftstore, claration|khcrysalis /Feather→FTStuff/FTStore, then generic capitalized Feather→FTStore.
  • Leaves intentionally untouched: the whole of web/ and .github/workflows/web-*, the all-caps FEATHER_* build variables in the xcconfig — invisible internals, referenced from both project.pbxproj files and both dev-repo workflows, and kept upstream-equal so the rare sync doesn't conflict in exactly the files that are most annoying to merge. Renaming them is a defensible hard-fork cleanup, but it is a build-config change no static check here can verify — do it only if asked, and get it built on a Mac, image assets / icons (referenced by imageset name, not filename), LICENSE/ios/LICENSE_ELLEKIT, README.md, CLAUDE.md and AGENTS.md (handled separately), .github/workflows/ios-dev-repo.yml (names both brands on purpose), submodule/package names (Zsign, IDeviceKitten, NimbleKit, AltSourceKit), and attribution handles (claration, khcrysalis, sponsor links, icon-author "Samara").
  • Three phrases naming upstream on purpose are guarded by a negative lookahead in the generic Feather→FTStore rule: Feather Developer (About credits), Feather authors (the comment above them), and Feather, made approachable (the Manager tagline, which must stay identical to app-repo.json's localizedDescription). The guard is line-local — never let one of those phrases wrap across a line break.
  • app-repo.json brand fields via jq; README.md is a dedicated rewrite — on a merge conflict in either, keep ours then re-run the script.

GPL-3.0

Keep LICENSE intact, preserve copyright/attribution, keep source public. Credit Feather (README "Acknowledgements" + an app-repo news entry).

Validation (Linux-local)

python3 -c "import json; json.load(open('app-repo.json'))"          # valid JSON
python3 -c "import json; json.load(open('manager/app-repo.json'))"
python3 -c "import plistlib; plistlib.load(open('ios/FTStore/Resources/Info.plist','rb'))"
python3 -c "import plistlib; plistlib.load(open('manager/FTStore/Resources/Info.plist','rb'))"
python3 -c "import glob,yaml; [yaml.safe_load(open(f)) for f in glob.glob('.github/workflows/*.yml')]"
grep -rn "thewonderofyou.Feather" ios app-repo.json                 # expect: none
grep -n "Feather" ios/FTStore.xcodeproj/project.pbxproj             # expect: none
bash scripts/apply-rebrand.sh && git diff --quiet                   # idempotent: no diff on 2nd run

Old-brand sweep. jq must be installed or apply-rebrand.sh silently skips its whole app-repo.json block (it is guarded by command -v jq) and the idempotency check above proves nothing about that file.

grep -rni "ftrepo" ios manager web .github scripts --exclude=package-lock.json

Five hits are deliberate and must survive:

  1. web/README.md — the "Migrating from FTRepo" section.
  2. web/src/lib/settings/backup.ts — LEGACY_SETTINGS_BACKUP_TYPES.
  3. manager/…/ManagerConfig.swift — manager.ftrepoBaseURL, the pre-rename UserDefaults key, read once and migrated.
  4. ios/…/DownloadManager.swift — ftrepo.xyz in the User-Agent allowlist.
  5. .github/workflows/web-delete-release.yml — matches old X.Y image tags.

For web/:

cd web && npm ci && npx prisma generate && npx tsc --noEmit && npx eslint .
find src -name '*.test.ts' -exec npx tsx --test {} +          # 45 tests
npm run build

Build verification is per product, all on macOS: ios-release.yml / make in ios/ for the app, manager-release.yml / make in manager/ for the Manager, web-docker-build.yml for the server.