The console now ships the way the app and the Manager do: every push touching admin/ builds an unsigned IPA and publishes it, together with a generated AltStore source, to a rolling `admin-dev` prerelease. - admin/app-repo.json is the source template the workflow rewrites with jq, same as manager/app-repo.json. - The tag is `admin-dev`, distinct from `dev` and `manager-dev` so the three builds don't overwrite each other's assets. - admin-build.yml is gone; the dev workflow covers the compile check it existed for, and running both would build every admin/ push twice. The repo is private, so "published" here means reachable by whoever already has access — which is the whole distribution model for an operator console. There is still no web/ proxy and no in-app updater: this app only ever calls the session-gated /api/* routes.
15 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.mdintentionally 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 inAboutView.swift, the "Built on Feather" entry inapp-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.shso the rebrand never mangles it — do not remove it (orCLAUDE.md) from that script's exclusion list.
- Brand constants: app name
FTStore· bundle idapp.ftstore· developerFTStore· websiteftstore.app· repoFTStuff/FTStore. The project is credited as a project, never as a person — there is no maintainer handle in the About screen,app-repo.jsonor the READMEs, and nothing should reintroduce one. - 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
Four 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 server — 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. One screen, no account. Was the separate FTStore-Manager repo. |
No — forked from ios/, not from Feather |
admin/ |
FTStore Admin (app.ftstore.admin) — the operator console for web/ as its own iOS app: the /admin dashboard on a phone, behind the server login. Split out of manager/ so a normal user never meets an operator surface. |
No — ours alone, and no Feather lineage |
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.
admin/ has no proxy of its own: it talks to the same /api/* routes the web
dashboard uses, gated by the session rather than by a UA. Its AltStore source
lives on the admin-dev prerelease of this private repo, so "published"
there means "reachable by someone who already has access" — which is the whole
distribution model for an operator console.
Three iOS apps, three audiences. ios/ is for everyone, manager/ is for
getting ios/ onto a device, admin/ is for the person who runs the server.
Never route an operator feature back into ios/ or manager/ — the reason
admin/ exists is that a normal user should never be able to reach one. And
never bake a credential into admin/: its only compiled-in value is the base
URL, and the server session is the whole security model.
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 dropsFeather/,Feather.xcodeproj,Makefile, … next toios/. Step 0 ofapply-rebrand.shrelocates them, folding into ours on collision. Never hand-move them. -
On a merge conflict, keep ours in
.gitmodules(ios/Zsign, notZsign),.gitignoreand.github/workflows/. -
web/,manager/andadmin/must never be touched by the rebrand. The script excludes^(web|manager|admin)/and.github/workflows/(web|manager|admin)-. For the two app directories this is load-bearing: the generic rule would rewrite theirapp.ftstore.manager/app.ftstore.adminidentities and mangle the Feather lineage in the Manager's docs. -
Release tags are namespaced, and nothing may reuse another's:
Tag Product Built by v*the app ios-release.ymlweb-v*the FTStore Admin server web-release.ymlmanager-v*FTStore Manager manager-release.ymladmin-v*the FTStore Admin app admin-release.ymldevthe app's rolling prerelease ios-dev-repo.ymlmanager-devthe Manager's rolling prerelease manager-dev-repo.ymladmin-devthe console app's rolling prerelease admin-dev-repo.ymlThe three
devtags are deliberately distinct: the app and the Manager were separate repos where both were calleddev, and sharing one here would make each build overwrite the others' release assets. -
The server domain is
ftstore.app, hardcoded in all three apps (ios/…/ManagerRepoClient.swift,…/Storage+Sources.swift,manager/…/ManagerConfig.swift,admin/…/AdminConfig.swift).DownloadManager.swiftstill allowlists the pre-renameftrepo.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-appand-worker. The Compose project is pinned toname: ftstoreso volumes stayftstore_*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 intomainand pushmain.) - 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-Bytrailer (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--mergeonly if the user asks for a wholesale catch-up. Either way, re-runscripts/apply-rebrand.shafterwards 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
all-caps build variables were renamed FEATHER_* → FTSTORE_* for exactly this
reason; apply-rebrand.sh re-applies that rename, so a sync cannot quietly put
the upstream spelling back.
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/(bothFeather*andFTStore*spellings, plusMakefile,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*insideios/(xcodeproj, xcworkspace, xcconfig, scheme, source dir,FeatherApp.swift, bridging header, entitlements, and the CoreData modelFTStore.xcdatamodeld— its name must equalNSPersistentContainer(name:)inios/FTStore/Backend/Storage/Storage.swift). - Re-points the workspace's
README.md/LICENSErefs at../, since the workspace sits one level below them now. - Rewrites identifiers/URLs:
thewonderofyou.Feather→app.ftstore,claration|khcrysalis /Feather→FTStuff/FTStore, then generic capitalizedFeather→FTStore. - Renames the all-caps build variables
FEATHER_*→FTSTORE_*— but only where it reaches, i.e.ios/. The same variables inmanager/and thegrepin both dev-repo workflows were renamed by hand and are outside the script's scope; they cannot drift, because neither is ever merged from upstream. The trailing underscore in that rule is load-bearing: bareFEATHERalso appears in the GPL-3.0 modification notices atopServerInstaller*.swift("MODIFIED FOR USE FOR FEATHER"), which are licence attribution and must never be rewritten. - Leaves intentionally untouched: the whole of
web/,manager/andadmin/and.github/workflows/{web,manager,admin}-*, image assets / icons (referenced by imageset name, not filename),LICENSE/ios/LICENSE_ELLEKIT,README.md,CLAUDE.mdandAGENTS.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→FTStorerule:Feather Developer(About credits),Feather authors(the comment above them), andFeather, made approachable(the Manager tagline, which must stay identical toapp-repo.json'slocalizedDescription). The guard is line-local — never let one of those phrases wrap across a line break. app-repo.jsonbrand fields viajq;README.mdis 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 json; json.load(open('admin/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 plistlib; plistlib.load(open('admin/FTStoreAdmin/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 admin web .github scripts --exclude=package-lock.json
Five hits are deliberate and must survive:
web/README.md— the "Migrating from FTRepo" section.web/src/lib/settings/backup.ts—LEGACY_SETTINGS_BACKUP_TYPES.manager/…/ManagerConfig.swift—manager.ftrepoBaseURL, the pre-rename UserDefaults key, read once and migrated.ios/…/DownloadManager.swift—ftrepo.xyzin the User-Agent allowlist..github/workflows/web-delete-release.yml— matches oldX.Yimage 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, admin-dev-repo.yml / make in admin/ for the console app,
web-docker-build.yml for the server.
admin/ links no Swift package at all — only system frameworks. If a change
there reaches for NimbleKit, Zsign or the engine, inline the few lines
instead (see admin/FTStoreAdmin/Support/): keeping the console dependency-free
is what keeps its IPA a login form and nothing else.