README is a short front page (about 120 lines) with one real visual per section; every former section moved unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth, studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools: tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
408 lines
33 KiB
Markdown
408 lines
33 KiB
Markdown
# storebound desktop (apps/desktop)
|
|
|
|
A Tauri 2 window around the storebound CLI: every app in a workspace, on iOS and Android, in one place. It is in
|
|
development; this page is for contributors.
|
|
|
|
## Shape
|
|
|
|
- **One source of logic.** The app never calls a store itself. It runs the storebound CLI as a sidecar
|
|
(`storebound serve --stdio`, compiled with `bun tools/build-sidecars.ts`) and asks it for everything.
|
|
- **App-centric.** One app = one product with up to two platforms (iOS bundle id / App Store Connect app id, Android
|
|
package name), one `storebound.config.ts` per app folder, many apps per workspace. Navigation: Portfolio -> App
|
|
(Releases, Catalog, Prices, Listings, Store art, Health, Reviews, TestFlight, Growth, App info, Checks) plus the
|
|
workspace screens (App folders, Accounts, Dependencies, Settings).
|
|
- **Release trains.** A version (for example 2.3.0) spans both stores: iOS build and Android versionCode, per-store
|
|
state (TestFlight, review, live, phased release, rollout fraction). `portfolio.status` returns per app the current
|
|
train (highest version live on a store) and the next one (highest version above it anywhere).
|
|
- **Catalog parity.** Products (and subscriptions, once the config has them) are defined once; `portfolio.status`
|
|
compares the config with both stores (ids missing on a store, ids on one store only).
|
|
|
|
Types of the protocol: `cli/lib/desktop-protocol.ts`; pure portfolio logic: `cli/lib/portfolio.ts`.
|
|
|
|
## Safety model
|
|
|
|
- The sidecar's first request is `session.hello` with a 32-byte secret the Rust host makes per start and writes to
|
|
the sidecar's stdin. Write methods are dry runs unless the call carries a confirm token: HMAC-SHA256 over the
|
|
method, the canonical `{ project, args }` and a 60 s expiry, single use. The host mints one only after the owner
|
|
pressed Apply in a native dialog that shows the summary and the dry run (`write_with_confirm`).
|
|
- No method releases an approved version, publishes, cancels a submission, sends a build to external testers or
|
|
removes testers. Under `serve`, the CLI's own terminal confirmations answer no, with one exception: the **owner
|
|
actions** (`owner.play_rollout` share / halt / resume / complete, `owner.ios_phased` on / pause / resume /
|
|
complete, `owner.reply_send` one approved draft, `owner.testflight_testers` adds the testers of
|
|
`.storebound/testers.csv`, `owner.prices_import` and `owner.prices_play_products` / `_play_subscriptions` / `_apple_subscriptions` with `--allow-price-changes`,
|
|
`owner.ppo_start` / `owner.ppo_stop`, and **sending for review**: `owner.apple_submit` (`apple submit --build n`),
|
|
`owner.apple_resubmit` (`apple resubmit --build n [--resolve-iaps]`), `owner.apple_iap_submit` (`apple iap-submit
|
|
[--with-version]`) and `owner.play_production` (`play release --version-code n --production completed | inProgress
|
|
| draft`)). They are desktop-only (never MCP tools), dry runs from the window like every write, and their real run
|
|
needs the host token. Only then serve sets `STOREBOUND_OWNER_CONFIRMED` for that one child, which answers the
|
|
command's question, and adds the owner-only flags (`--yes` of apple submit / resubmit, `--allow-price-changes`);
|
|
`OWNER_ALLOWED_ARGS` names the one action each such flag belongs to and a dry run never carries one. Builds and
|
|
versionCodes are picked by number in the window, never "the latest". The native dialog names the action on its
|
|
button ("Yes: Submit for App Review"); for the submit actions it says that the app goes to review and may go live
|
|
by itself once approved. Play's "Send changes for review" (Publishing overview) has no API: the Releases tab shows
|
|
its console plan and a deep link instead. `tests/desktop-d3.test.ts`, `tests/serve.test.ts` and
|
|
`src-tauri/tests/d3_writes.rs` pin the token binding of every write the window offers and that the MCP server has
|
|
none of these.
|
|
- The webview gets no plugin API. It may call the app commands (each re-checks the window label and the page origin
|
|
in Rust) and listen to two events. Strict CSP, no remote content. `src-tauri/tests/security.rs` pins all of it.
|
|
- Store keys (Play service account JSON, App Store Connect key id, issuer id and .p8) are picked in native dialogs,
|
|
stored in the OS keychain (`keyring`), never shown again and passed to the sidecar per call in memory only.
|
|
|
|
## Screens and data (D2)
|
|
|
|
- **Portfolio**: KPIs (apps, live versions, rolling out, in review, needs attention), one row per app with the version
|
|
reaching users on each store and its place on the track (build, testing, review, live, rollout %), the next
|
|
version, the last saved crash rate as a sparkline, catalog parity and the config check; then a "needs attention"
|
|
list (problems first, then warnings, store read errors, rejected or halted releases, catalog drift, health near a
|
|
threshold).
|
|
- **App**: Releases (release timeline, both stores on one track per version; iOS phased release day and share from
|
|
`apple status --json`), Catalog (parity matrix from `catalog.diff`: matches, differs, missing, not sold there, not
|
|
compared; price gaps per region), Listings (`project.listings`: per language and store, fields against the limits,
|
|
rendered screenshots), Health (`health`: daily user-perceived crash and ANR rate against the Play thresholds,
|
|
worst devices, top issues, App Store metrics and signatures), Reviews (`reviews.list` + `reviews.drafts`: star
|
|
distribution, filters, the local draft with its send checks, Approve via `reviews.approve`), TestFlight
|
|
(`apple.testflight_status`), Checks (offline `scan`: rejection risks with guideline, evidence file:line, fix
|
|
and skill, owner-accepted ones marked; offline `check`; `validate` with fix hints).
|
|
- **First-run guide**: what the app does, add or start an app folder (`project.init` = `storebound init --profile`,
|
|
confirmed in a native dialog), store keys into the keychain and which app uses which, local tools, first read.
|
|
- **Command palette** (Cmd/Ctrl+K): apps, tabs, screens and actions. Cmd/Ctrl+1 Portfolio, +2-5 workspace screens,
|
|
Cmd/Ctrl+Shift+[ ] app tabs, arrow keys in the tab list. Toasts show background jobs with progress and Cancel.
|
|
- **Unknown, never false.** Store reads run on demand (they use the account's API quota) and are saved in app data
|
|
(`cache.sqlite`, results only, never keys); a screen opens with the saved read and its time ("Last read 3 h ago,
|
|
saved copy"), a failed read keeps the previous one and says so, and nothing that was not read is shown as fine.
|
|
Settings clears the saved reads.
|
|
- **Jobs.** The window tags a call with a job id; the host maps sidecar request ids to it, so progress lines reach the
|
|
right toast, and `job_cancel` sends `$/cancelRequest` (or stops a download). Live portfolio reads run up to four
|
|
apps at a time with their own accounts and report n / total.
|
|
- **i18n**: English and Turkish (`src/i18n`), Settings picks the language (default: the system's); `tools/i18n-check.ts`
|
|
(CI) checks keys, empty messages and placeholders. **Theme**: text tokens keep 7:1 contrast in both themes
|
|
(`tests/theme.test.ts`), motion at most 200 ms and off with reduced motion.
|
|
- **Charts, icons, art: libraries only, no hand-drawn SVG** (owner rule, guarded by `tests/no-handmade-svg.test.ts`).
|
|
Charts use visx (`@visx/*`, MIT; `src/ui/charts.tsx`, `src/ui/LineChart.tsx`): Growth's indexed lines with the 100
|
|
baseline, listing-change markers and a crosshair tooltip with the real values; Health's crash / ANR series against
|
|
the bad behavior threshold (crosshair + tooltip, arrow keys when focused); the portfolio's compact sparklines; the
|
|
rollout / phased release bar. Widths follow the container (`useParentSize`), colours are the theme tokens, nothing
|
|
animates. Icons are Phosphor (`@phosphor-icons/react`, MIT), one per-icon import each
|
|
(`@phosphor-icons/react/dist/csr/<Name>`, so only the icons used are bundled); `src/ui/icons.tsx` maps the app's icon
|
|
names to them. One weight, "regular"; "fill" only marks state (the active nav item, notices, rating stars). Store
|
|
marks are the stores' own logos from simple-icons (CC0 data), vendored unchanged into `src/ui/store-marks/` by
|
|
`bun tools/site-icons.ts` and shown as a CSS mask in the neutral text colour, always next to the store's name. They
|
|
identify the store only: storebound is not affiliated with Apple or Google, and the marks are never styled as its
|
|
own branding. App Store is a trademark of Apple Inc.; Google Play is a trademark of Google LLC. Empty states
|
|
(`src/ui/illustrations.tsx`) are a Phosphor icon on a token-coloured tile with an optional bold badge icon, no
|
|
artwork.
|
|
The fixtures' app icons and screenshot stand-ins are PNGs from `bun tools/fixture-art.ts` (Phosphor "fill" icons
|
|
on CSS gradients, photographed by Chromium). The brand mark (`src/ui/brand.tsx`) is a PNG
|
|
(`src/ui/logo-mark.png`, from `tools/brand-assets.py`, see `brand/README.md`): no inline SVG at all. Bundle cost: about +97 kB minified, +31 kB gzip for visx (d3-scale / d3-shape and
|
|
@visx/text for the axes). The Phosphor icons cost about 168 kB minified / 41 kB gzip more than the lucide set they
|
|
replaced (each per-icon module carries all six weights; 864 kB / 247 kB gzip for the whole webview script).
|
|
|
|
## Studios, accounts and store-only apps (D3)
|
|
|
|
### Accounts and studios
|
|
|
|
- A **studio** is a name, a colour and an optional logo. It groups any number of store accounts:
|
|
- a Play service account, which can be invited to several Play developer accounts and then sees all their apps;
|
|
- an App Store Connect team key, one per team.
|
|
- Keys stay in the keychain. The state file keeps labels and studio ids only.
|
|
- `storebound apps check` gives each account's health, with the fix in words:
|
|
- ok;
|
|
- no-apps;
|
|
- permission (Play: "invite <service account> in Play Console > Users and permissions with ...");
|
|
- revoked;
|
|
- api-disabled.
|
|
- `storebound apps list` reads the apps an account can see:
|
|
- App Store Connect: `GET /v1/apps`, the newest version, and an icon from the newest build's `iconAssetToken`,
|
|
fetched by the CLI and returned as a data URL;
|
|
- Play: the Reporting API `apps:search`.
|
|
- Serve exposes both as `apps.list` and `account.check` (kind `account`, one account's key per call, run in an
|
|
empty temp folder). The host caches app lists under `acct:<id>`.
|
|
|
|
### One card per app
|
|
|
|
`src/lib/identity.ts` builds one card per app, pure and tested:
|
|
|
|
1. A folder takes the store apps its config names. Order: an explicit link, then the App Store app id, then the
|
|
bundle id or package name. The folder's mapped accounts win.
|
|
2. The remaining store apps pair iOS + Android, in this order:
|
|
- manual pairs;
|
|
- the same id;
|
|
- a close id (`.ios` / `.android` suffixes, case, punctuation);
|
|
- the same name.
|
|
|
|
Unpairs block automatic pairs.
|
|
3. What is left is a single-store app.
|
|
|
|
Overrides (pairs, unpairs, links, studio moves, the ignore list of apps, folders and whole accounts) live in the
|
|
host state file (`overrides_set`, every key checked in `workspace.rs`).
|
|
|
|
### Store-only apps
|
|
|
|
- A store-only app has no folder. It is read through a **store target**:
|
|
`store:ios:<account>:<app id>:<bundle>;android:<account>:<package>`.
|
|
- The host checks the target and the account kinds, and passes `virtual: { name, apple, play }`. The sidecar writes
|
|
a minimal `storebound.config.json` (store ids, placeholder links) into its temp folder.
|
|
- Only `apple.status`, `play.status`, `health`, `reviews.list`, `apple.testflight_status` and `apple.ppo_status` work
|
|
this way (tabs Releases, Health, Reviews, TestFlight, Growth), and `portfolio.status` takes store-only entries.
|
|
- "Link a folder", "Start an app folder here" or "Import from fastlane" turn it into a full app.
|
|
|
|
### Settings
|
|
|
|
- Studios and accounts.
|
|
- Pairing and the ignore list.
|
|
- Store reads: on launch, every 15 min to 12 h (off by default), apps at a time 1-8, icons.
|
|
- Offline cache: size, keep 7 / 30 / 90 / 365 days or always, clear.
|
|
- Language, theme and motion.
|
|
- Notifications, after a read, never on the first one: review / release state changes, new 1-2 star reviews, crash
|
|
or ANR over the Play threshold.
|
|
- Dependencies and the CLI sidecar.
|
|
- Privacy, with a button that reveals the data folder.
|
|
- Shortcuts.
|
|
- Updates: channel (stable / beta), check and install (D5).
|
|
- Reset: native confirmation, then the keychain entries and every saved state are removed.
|
|
|
|
### Writes in the window
|
|
|
|
Each write is a dry run, then the native dialog, then the real run, with one progress toast and Cancel:
|
|
|
|
- **Listings tab:** Play listing text, App Store version and text, Play graphics, App Store screenshots and
|
|
previews; local render jobs (screenshots, previews, icons, gallery).
|
|
- **Catalog tab:** Play products and subscriptions, App Store IAPs and subscriptions, price export; price changes of
|
|
live products (`owner.prices_play_products`, `owner.prices_play_subscriptions`, `owner.prices_apple_subscriptions`, owner actions: a normal sync holds them back and exits 3, shown as "Waiting
|
|
for your decision").
|
|
- **Prices tab:** the price sheet compared with the stores and applied (`owner.prices_import`, an owner action).
|
|
- **App info tab:** Play Data safety upload (the deletion question as the CSV answers it and where the answer comes
|
|
from), App Store app info, export compliance, App Review details; "Console steps without an API": every form of
|
|
`console plan` (App Store app record, age rating, App Privacy; Play Create app, store settings category, every App
|
|
content card, content rating, target audience, production countries, production release) with its plan panel
|
|
(start page, steps, answer sheet, never-press buttons, Copy plan for an AI agent) and the read-only check
|
|
(`console.verify`, page text pasted or the API), plus "Fill in console" where the scripted path exists.
|
|
- **Growth tab:** analytics snapshot and report request, PPO create and treatment upload; PPO start / stop
|
|
(`owner.ppo_start`, `owner.ppo_stop`, owner actions).
|
|
- **Store art tab:** render, review page.
|
|
- **Releases tab:** rollout and phased release owner actions; **Send for review**: App Store submission state from
|
|
`apple status` (waiting / in review, UNRESOLVED_ISSUES with its items, empty drafts labelled harmless), the uploaded
|
|
builds by number, Submit for review / Resubmit (with the rejected items to mark resolved) / in-app purchases; Google
|
|
Play versionCodes by number, Send to production (everyone, staged, or a draft for a never-published app), the
|
|
managed-publishing warning and the console plans for production countries and "Send changes for review". Each
|
|
button shows the plan (dry run) in the window first (`ui/OwnerAction.tsx`), then the native dialog; progress lines
|
|
of the real run stream into the panel.
|
|
- **TestFlight tab:** What to Test, internal groups, testers, "Create missing groups" (`apple testflight groups
|
|
sync`, dry run first).
|
|
- **Dependencies screen:** the Android SDK as `play build` finds it (`system.tools` `androidSdk`).
|
|
- **Job toasts:** the command's progress lines; upload lines ("312/890 MB (35%), part 4/12") drive the bar, and the
|
|
last 40 lines are in the toast's log.
|
|
- **Reviews tab:** reply editor (`reviews.reply`), approve, send (owner action).
|
|
|
|
### The same model in the terminal
|
|
|
|
The CLI workspace (`storebound.workspace.json`, docs/studios.md "Many apps and studios") holds the same studios, accounts
|
|
(credential profile names instead of keychain entries), folders and overrides. The pairing rules
|
|
(`cli/lib/identity.ts`) and the release lanes (`cli/lib/train.ts`) are one implementation for both; the window
|
|
re-exports them (`src/lib/identity.ts`, `src/lib/model.ts`). `storebound ws desktop import` reads `state.json` into a
|
|
workspace file (the keychain is never read; the profiles to create are listed); `storebound ws desktop export --merge
|
|
--apply` adds a workspace to `state.json` while the app is closed (backup kept, nothing removed, new accounts get
|
|
their keys in the Accounts screen). Project ids in an exported state are stable per folder path (16 hex).
|
|
|
|
### Fill in console
|
|
|
|
Some store forms have no API: the App Store app record (New App), the age rating, App Privacy, Play's Create app
|
|
form, and the Play content rating and target audience questionnaires. The App info tab has a **Fill in console**
|
|
button for each (App Privacy card, App info card for the age rating, a "Play Console forms" card, and a notice when the config has no
|
|
App Store app id yet). It runs `storebound console fill <form>` through the desktop-only method `console.fill` (a
|
|
write, never an MCP tool):
|
|
|
|
1. The click runs the dry run (`console fill <form> --dry --json`): nothing opens. A panel lists the answers from
|
|
the config; the ones only the owner knows are marked "You answer this". It shows the note: "Console pages
|
|
change. This is AI-assisted: review every field in the browser and press the final button (Publish, Submit, Save
|
|
or Create) yourself. storebound moves through the pages for you and never submits or publishes; App Privacy
|
|
answers stay a draft until you press Publish."
|
|
2. **Open the browser** goes through `write_with_confirm`: the host repeats the dry run and shows the answers and the
|
|
note in a native dialog ("Open the browser" / Cancel). Only that button mints the confirm token.
|
|
3. The real run opens a visible Chromium with its own profile. The owner signs in (storebound never types passwords);
|
|
storebound fills the fields, highlights the uncertain ones, clicks the navigation (Next, App Privacy's Get Started /
|
|
Edit / data types and its draft Save) and never clicks Submit, Publish, Create or a final Save. The
|
|
run lasts until the owner closes the browser: no timeout in serve, the runner or the host. Progress lines show in
|
|
the panel; **Cancel: close the browser** sends `$/cancelRequest`, which stops the command and its browser.
|
|
|
|
The browser is the same as in the terminal (cli/console/session.ts): a Chrome running with remote debugging, else
|
|
storebound's own profile (the owner signs in there once; the native dialog says so) in the desktop's Chrome for
|
|
Testing (`STOREBOUND_CHROME`), started by storebound without automation switches and attached with the bundled
|
|
playwright-core. The desktop never copies the owner's Chrome cookies (Google binds them to the device; see the
|
|
console-autofill skill); `--use-chrome-session` is a terminal-only opt-in. The tool's `fresh_profile: true` passes
|
|
`--fresh-profile` (skip attach). storebound opens the form itself after sign-in (navigation clicks only).
|
|
The own profile is one per store (every studio shares the one login) and keeps the sign-in between runs: launched with
|
|
session restore, closed gracefully on Cancel. With one login in several Play developer accounts or App Store Connect
|
|
teams, the run finds the account that has the app and remembers it in `~/.storebound/console-accounts.json`
|
|
(`{ version, play: { <package>: { developerId, account, at } }, apple: { <bundle id>: { team, teamId?, at } } }`, ids
|
|
and account names only), the same file the desktop reads. A workspace account's `developerId` (play) / `teamId`
|
|
(apple) is passed as `STOREBOUND_PLAY_DEVELOPER_ID` / `STOREBOUND_APPLE_TEAM_ID`.
|
|
|
|
## Every CLI area in the window
|
|
|
|
| CLI | where in the window |
|
|
|---|---|
|
|
| `prices export --json` (new, read-only) | Prices: product x territory matrix (both stores, Apple manual / equalized), per-territory table with gaps |
|
|
| `prices export` | Prices: "Write store/prices.csv" (confirm flow); "Export prices CSV" saves the same sheet anywhere |
|
|
| `prices import --dry --json` / `prices import` | Prices: compare store/prices.csv or a picked CSV (`price_sheet_pick`), diff table, Apply = `owner.prices_import` |
|
|
| `inspect --declarations --json` (new) | App info: Data safety, App Privacy suggestions, app info, encryption, review details, "ask the owner" items |
|
|
| `play data-safety` | App info: Upload (keeps `.storebound/data-safety.uploaded.csv`, so the tab shows changes since; Play cannot read the form back) |
|
|
| `apple appinfo`, `apple encryption`, `apple review-details` | App info: Push buttons (confirm flow) |
|
|
| `console fill <form> --dry --json` / `console fill <form> --json` (desktop-only `console.fill`) | App info: "Fill in console" (answers panel, native dialog, visible browser the owner saves in) |
|
|
| `apple ppo status / create / upload / start / stop` | Growth: tests table, create, upload, Start / Stop owner actions |
|
|
| `apple status --json` (review submissions, UNRESOLVED_ISSUES items, empty drafts, builds) | Releases: Send for review, App Store card |
|
|
| `apple submit --build n`, `apple resubmit --build n [--resolve-iaps]`, `apple iap-submit [--with-version]` | Releases: Submit for review / Resubmit / Submit in-app purchases (owner actions) |
|
|
| `play status --json`, `play release --version-code n --production ...` | Releases: Google Play card, Send to production (owner action) |
|
|
| `console plan <form> --json`, `console verify <form> --json` | App info: Console steps (every form); Releases: production countries, Send changes for review |
|
|
| `apple testflight groups sync` | TestFlight: Create missing groups |
|
|
| `play products` / `play subscriptions` / `apple subscriptions --allow-price-changes` | Catalog: Price changes (owner action) |
|
|
| `doctor` Android SDK | Dependencies: Android SDK card |
|
|
| Play store listing experiments | no API: Growth links Play Console and the variant renders |
|
|
| `analytics compare --json`, `snapshot`, `setup` | Growth: chart (reach, installs, conversion, listing changes as markers), before / after table, snapshot and setup buttons |
|
|
| `shots gallery --json`, `inspect --render` (new) | Store art: grid per variant / language / size, issues, lightbox; images load lazily one at a time as data: URLs (never cached) |
|
|
| `shots render`, `shots gallery` | Store art: Render (language, variant), Write review page, Open review page / folder (`project_reveal`) |
|
|
| `inspect --listing-text --json` (new) | Listings export (Markdown) |
|
|
| `import fastlane` | First-run guide, App folders, store-only app banner (`project_import_fastlane`: plan in a native dialog first) |
|
|
|
|
**Exports.** Every screen registers what it can save (`src/ui/Exports.tsx`): the "Export" menu at the top right of
|
|
the top bar and the "Export" group of the command palette. Files: prices CSV (the `prices import` format), listings
|
|
(md), reviews CSV, health report (md), catalog parity (md), store readiness report (md), store declarations (md),
|
|
Data safety CSV, analytics CSV, portfolio CSV. `export_save` writes only through a native save dialog (Downloads
|
|
first), only csv / md / json / txt, sanitized names, at most 16 MB; the reviews export shows a native personal-data
|
|
note first.
|
|
|
|
## UX audit (full CLI coverage)
|
|
|
|
- **Two clicks.** From an app page every action is one tab plus one button: prices, Data safety, app info, review
|
|
details, PPO, analytics, renders. The command palette jumps to Prices and Store art and runs every export.
|
|
- **One place for exports.** Before, the only export was a write button in Catalog. Now each screen's exports sit in
|
|
one menu at the top right (the same spot on every screen) and in the palette; disabled items say why.
|
|
- **Buttons.** Card head actions go from secondary (ghost: open, show) to the primary action at the right end; owner
|
|
actions are named on the button and in their native dialog. App folders: Import from fastlane, Start an app folder,
|
|
Add app folder (primary) in the page header, the same three in the empty state; each folder row has Open and Show.
|
|
- **Empty states say what to do.** Prices not read (Read prices), no products (where to add them), no snapshots (what
|
|
a snapshot is, the bucket for Play), no PPO tests (create from a variant, or add variants), no renders (Render), no
|
|
Data safety answers (which skill), store-only app (Link a folder / Start / Import).
|
|
- **Errors say how to fix.** `fixHint` (src/lib/views.ts) maps known CLI errors (keys, permissions, config values,
|
|
ffmpeg, Chrome, renders, snapshots, bucket, editable version, fastlane folder, price sheet, price points, network,
|
|
history) to the step that fixes them, shown under read errors and in failure toasts.
|
|
- **Ask, never guess.** App info opens with the questions only the owner can answer (age rating, App Privacy linked /
|
|
tracking, encryption, sign-in, contact), highlighted in amber, each with how to answer.
|
|
- **First run.** The guide now starts with the studio (pick or create, with a colour), so the keys added two steps
|
|
later land in it; the app step offers add / start / import from fastlane side by side.
|
|
- **Tabs.** Eleven app tabs fit 1440 px; narrower windows scroll the tab bar sideways instead of wrapping.
|
|
|
|
R&D on a fastlane importer and a RevenueCat adapter: `docs/research/fastlane-revenuecat.md`.
|
|
|
|
## Dependencies screen
|
|
|
|
storebound needs no local tool for store work. Screenshots, app previews, icons and captures use:
|
|
|
|
| tool | used for | how the app gets it |
|
|
|---|---|---|
|
|
| ffmpeg, ffprobe | icons, renders, app previews, frame crops | download into the app data folder (planned, see below) |
|
|
| Chrome / Chrome for Testing | screenshot renders, captions, web captures | system Chrome, or Chrome for Testing downloaded into the app data folder (planned) |
|
|
| Xcode command line tools, simctl | iOS builds, Simulator captures | link to Apple only |
|
|
| adb | Android emulator / device captures | link to the Android platform tools only |
|
|
| git | release notes, analytics compare | link only |
|
|
|
|
`system.tools` reports each with its path, version and source (`app` or `system`). The host passes app-managed
|
|
copies to the sidecar: `STOREBOUND_TOOL_PATH` (app data `tools/bin`, put in front of PATH for every command) and
|
|
`STOREBOUND_CHROME` (app data `tools/chrome/...`, used as Playwright's `executablePath`). Nothing is installed
|
|
system-wide and nothing is bundled in the installer.
|
|
|
|
`storebound doctor` prints the same detection (plus playwright) with the install commands for the OS.
|
|
|
|
Download sources (one click on the Dependencies screen or in the first-run guide; `src-tauri/src/downloads.rs`):
|
|
|
|
- **ffmpeg, macOS (arm64, x64):** release builds from ffmpeg.martin-riedl.de (static, notarized zip per tool).
|
|
- **ffmpeg, Linux (x64, arm64) and Windows (x64):** `ffmpeg-n<major>-latest-<os>-gpl` release builds from
|
|
github.com/BtbN/FFmpeg-Builds.
|
|
- **Verification:** the app ships a manifest (`src-tauri/tools-manifest.json`, written by `bun tools/tools-manifest.ts`
|
|
before each desktop release, `--check` tests the URLs) with the exact URL, size and SHA-256 of each archive; a
|
|
download whose hash differs is deleted. Every pinned hash has two independent sources: macOS archives are
|
|
downloaded and hashed by the tool and must equal the publisher's `.sha256`; BtbN archives (pinned autobuild tag)
|
|
use the digest GitHub computes for the release asset, which must equal the publisher's `checksums.sha256`.
|
|
Downloads and their redirects may only use HTTPS and the hosts in `downloads::DOWNLOAD_HOSTS`. Everything is
|
|
unpacked and checked in a staging folder; only a verified result replaces the installed copy, and the sidecar
|
|
restarts so it sees the new tool.
|
|
- **License:** these ffmpeg builds are GPL (v3 for the BtbN `gpl` variants). The app downloads them only on the
|
|
owner's click, from the original publisher, shows the license and the source link next to the button, and never
|
|
redistributes them, so the app itself stays MIT.
|
|
- **Chrome for Testing:** Google's official JSON endpoints
|
|
(googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json, Stable channel), archives
|
|
from storage.googleapis.com over HTTPS. Those endpoints publish no hashes, so the app checks the code signature after
|
|
unpacking (macOS `codesign --verify` with Google's team, Windows Authenticode); on Linux it checks the version banner.
|
|
Chrome for Testing is under Google's terms for Chrome / Chromium (BSD-style Chromium license plus third-party notices).
|
|
- **Progress and cancel** go through the same progress events as sidecar jobs; a cancelled download removes its
|
|
temporary file.
|
|
|
|
## Release, updates and the terminal bridge (D5)
|
|
|
|
- Releases: [desktop-release.md](desktop-release.md) (version authority, privacy gate, macOS signing and
|
|
notarization, Forgejo builds for Linux and Windows, storebound.dev hosting, publish).
|
|
- **Updates**: Settings > Updates checks storebound.dev (stable `/desktop/latest.json` or beta
|
|
`/desktop/beta/latest.json`) only when asked; Install shows a native dialog, then the updater downloads the bundle,
|
|
checks its minisign signature against `plugins.updater.pubkey` and restarts (`update_check`, `update_install`).
|
|
- **`storebound://open?path=<absolute folder>`** (sent by `storebound desktop` in an app folder): the host accepts
|
|
one plain absolute path (`policy::deep_link_open_path`), selects that app folder when it is in the list, otherwise
|
|
asks in a native dialog and adds it (`deep-link-folder` event). A folder without a storebound config gets a message,
|
|
nothing else.
|
|
- **`storebound-desktop --self-test`**: starts the bundled sidecar, checks the `session.hello` handshake, prints one
|
|
JSON line and exits, before Tauri starts (no window). The release runs it on the exact artifacts.
|
|
|
|
## Never a blank window (D6)
|
|
|
|
- **Frozen prototype.** `freezePrototype: true` makes Tauri run `Object.freeze(Object.prototype)` before any page
|
|
script. Dependencies that assign `x.constructor = ...` or `x.toString = ...` on a plain object (d3-color, d3-format
|
|
under the visx charts) then throw while the bundle loads ("Cannot assign to read only property"; WebKit:
|
|
"Attempted to assign to readonly property"), and the window shows only its background colour, before React and
|
|
before the sidecar starts. `apps/desktop/build/freeze-safe.ts` (a Vite plugin) rewrites those assignments in
|
|
dependencies into a helper that defines the property when the frozen prototype would refuse it.
|
|
- **Saved state versions.** `state.json` carries `version` (2). Files without it are format 1 (D1 to D3: projects and
|
|
accounts, later studios, overrides and settings) and are migrated once (`store::migrate`), the original kept as
|
|
`state.json.v1.bak`. An unreadable file is kept as `state.json.unreadable-<time>` and the app starts empty; entries
|
|
that do not parse are dropped one by one (named in host.log), settings missing a field keep the rest.
|
|
- **Folders that are gone** (moved, renamed, deleted, old worktrees): `projects_list` marks them `missing`; the app
|
|
shows "Folder not found" with Locate… (`project_locate`: keeps the id, accounts, links and saved reads) and Remove
|
|
from list. Portfolio reads skip them.
|
|
- **Error screens.** A React error boundary around the whole window and one per screen and app tab show the error,
|
|
Copy details, Reload and Reset saved state (keeps a backup: `state_reset` moves `state.json` to
|
|
`state.json.reset-<time>.bak` after a native confirmation; keys stay in the keychain). `public/boot.js` runs before
|
|
the bundle and shows the same panel in plain DOM when the bundle fails or the window stays empty.
|
|
- **Logs** in `<app data>/logs`: `host.log` (start, saved state and migration, sidecar start / stop with its last
|
|
stderr lines, panics) and `webview.log` (window.onerror, unhandled rejections, boundaries, through `log_webview`,
|
|
at most 60 entries a minute). Rotated at 512 KB (one `.1.log` kept); home folder shown as `~`, keys, tokens,
|
|
passwords and e-mail addresses replaced.
|
|
- **Boot check** (`bun tools/desktop-boot-check.ts`, in `bun run ci` and the macOS release): the built `dist/` in
|
|
headless Chromium with the release CSP and the frozen prototype, a mocked host answering from the saved state
|
|
fixtures (`apps/desktop/tests/fixtures/state`: empty, D1 with folders gone, D3 full; the host answers are written by
|
|
the Rust migration itself, `src-tauri/tests/state.rs`) and the real sidecar for reads. It opens every screen and
|
|
app and fails on any uncaught or console error, an error panel or an empty `#root`; a last case removes the bundle
|
|
and expects the boot.js panel. One browser (killed at the end), a page per case, the cases in parallel against the
|
|
one build, and no fixed sleeps: each step waits for its element, its text or a quiet window (no host call in
|
|
flight, no DOM change).
|
|
|
|
## Develop
|
|
|
|
```sh
|
|
bun install
|
|
bun tools/build-sidecars.ts --host # apps/desktop/src-tauri/binaries/storebound-<target triple>
|
|
cd apps/desktop && bunx tauri dev # Vite on 127.0.0.1:1420 + the Rust host
|
|
cd apps/desktop/src-tauri && cargo test && cargo clippy --all-targets -- -D warnings
|
|
```
|
|
|
|
Dev-only switches (ignored in release builds): `VITE_START=accounts|app:catalog|app:health:2|welcome:3|palette|...`
|
|
opens a screen first, `VITE_THEME=light|dark` and `VITE_LANG=en|tr` force theme and language, `VITE_AUTORUN=gallery`
|
|
starts the confirm flow on the first app (the same names work as `?start=...&theme=...` query parameters on the Vite
|
|
server). `STOREBOUND_DEV_PROJECTS=<dir>:<dir>` (debug builds) seeds the app folder list.
|
|
|
|
Fixture mode (dev builds only): `VITE_FIXTURES=1` answers every app command from `src/dev/fixtures.ts` (made-up
|
|
store data around the two demo apps; `=empty` for a first run, `=cold` for nothing read yet). The window shows a
|
|
"DEV FIXTURES" badge; release bundles do not contain it (`tests/webview.test.ts` checks `dist/`). Screenshots of
|
|
every screen in both themes: `VITE_FIXTURES=1 bunx vite --port 1431 &` then
|
|
`bun scripts/screens.ts --base http://127.0.0.1:1431 --out <dir>` (playwright with the installed Chrome).
|
|
Gotchas kept from earlier Tauri apps: Cmd+Q quits and the Window menu keeps an entry for the hidden window; platform
|
|
configs (`tauri.<os>.conf.json`) must not set `app.windows` (Tauri replaces the whole array); Linux sets
|
|
`WEBKIT_DISABLE_DMABUF_RENDERER=1` unless the user set it; a `bun build --compile` binary needs an ad-hoc re-sign on
|
|
macOS or the kernel kills it on start.
|