One product per machine: the CLI and the desktop app share one version, the store accounts and the app folder list

- one version: package.json's version is the desktop app's too (desktopVersion removed); tools/desktop-version.ts
  keeps the plugin manifests and the app's copies equal, desktop-release publish refuses another version, release
  order app first then npm publish; the unpublished desktop 0.1.0 candidate is noted in the docs and CHANGELOG
- storebound desktop installs (per user, no admin: macOS DMG -> /Applications or ~/Applications, Windows NSIS /S,
  Linux AppImage + .desktop with the storebound:// handler), updates an older app, notes a newer one (downgrade only
  with --force), asks once ([Y/n], --yes, exit 3 without a terminal), opens on the folder; --dry / --json / --download;
  the install steps are pure plans (cli/lib/desktop-install.ts); the version's own manifest dl/<version>/latest.json
- credentials: one store, the CLI's credential profiles in ~/.storebound/credentials; the desktop writes and reads
  them (profiles.rs, files 0600), moves old keychain accounts into profiles on first start, shows profiles the CLI
  made; precedence env > named profile > project file > default profile; config `account`, "account" in the
  project's credentials.json; storebound accounts list | import (dedup by key id / service account e-mail, key files
  copied 0600, project file keeps keystore and review contact, backup, --dry, never prints values) | use
- app folders: one list ~/.storebound/projects.json shared by the desktop (registry.rs), storebound desktop,
  apps folders | add | remove, ws init and ws without a workspace file
- desktop: notice when a folder pins another storebound version; storebound-desktop --version
- tests: version drift, desktop decisions and per-OS plans, credential precedence and import, registry (TS + Rust),
  keychain migration and profile round trip (Rust), boot check with old keychain state and the new shared state
This commit is contained in:
CRTkafa 2026-10-13 06:17:31 +08:00
parent bba46b5c7f
commit d2ce185441
60 changed files with 2443 additions and 246 deletions

View file

@ -29,7 +29,9 @@ run in the background with a log (`storebound shots render > .storebound/render.
| `ads scan --ios <path>` | local | AdAttributionKit / SKAdNetwork: ad network ids, postback copy endpoints, attribution windows and cooldown, conversion value updates; exit 1 on a blocker |
| `privacy-manifest --ios <path>` | local | PrivacyInfo.xcprivacy, required-reason APIs, SDKs without manifests |
| `api list \| coverage \| apple\|play <operation>` | API | every App Store Connect / Google Play operation (see [Every endpoint](api.md)); GET runs, writes are dry unless `--yes` in a terminal |
| `desktop [--download]` | local | open the storebound desktop app (in an app folder: add or select it there); not installed: the download for this system, `--download` saves it to ~/Downloads after checking its signature (never installs) |
| `desktop [--yes] [--dry] [--download]` | local + download | the desktop app of this CLI's version, opened on this folder: installs it for this user when missing (size, SHA-256 and signature checked; asks once, `--yes`), updates an older one, notes a newer one (`--force` to downgrade); `--download` only saves the checked installer |
| `accounts [list] \| import [--dry] \| use <profile>` | local | the store accounts of this machine (`~/.storebound/credentials`, shared with the desktop app); `import` moves a project's store keys into a profile (same key = same profile), never prints values |
| `apps folders \| add [<folder>] \| remove <folder>` | local | the app folders of this machine (`~/.storebound/projects.json`), the same list as the desktop app |
| `doctor [--json]` | local | which local tools are here (ffmpeg, Chrome, playwright, Xcode, adb, git) and how to install the missing ones |
| `icons` | local | App Store 1024 (no alpha), Play 512, web, Capacitor sources, Android legacy launcher icons |

View file

@ -13,23 +13,27 @@ How the desktop app (`apps/desktop`) is built, signed, checked and published. Fo
updater key. The app checks the signature before it replaces anything and installs only after the owner clicks
Install in a native dialog (Settings > Updates).
- **It brings its own CLI.** The app bundles the storebound CLI as a sidecar, so it never depends on an npm install.
That CLI is the npm package of the same version.
- **The npm package stays the CLI, the MCP server and the skills.** It contains no Tauri binaries.
- **Bridge: `storebound desktop`.**
- Installed: it opens the app. In an app folder it sends `storebound://open?path=<folder>`, and the app selects
that folder, or adds it after a native dialog.
- Not installed: it prints the download for this OS and CPU.
- `--download` saves the installer to ~/Downloads after checking its size, SHA-256 and minisign signature. It never
installs anything.
- **Bridge: `storebound desktop`** installs, updates and opens the app of the CLI's own version (docs/desktop.md,
"One product with the CLI"). It reads `storebound.dev/desktop/dl/<version>/latest.json` (written by the publish
script next to the version's files) and falls back to the channel's `latest.json`. `--download` only saves the
checked installer to ~/Downloads.
## One version
## One version, one release
`desktopVersion` in the root `package.json` is the desktop app's version. The npm package's own `version` belongs
to the CLI and moves on its own.
The CLI and the desktop app are one product with one version number: `version` in the root `package.json`. 0.8.0 is
the first release of both under one number. (The desktop had its own `desktopVersion`, 0.1.0; its release candidate
in `~/Desktop/storebound-dist/desktop-0.1.0` was never published, so no installed app or link depends on it.)
- `bun tools/desktop-version.ts` checks the copies: `apps/desktop/package.json`, `tauri.conf.json`, `Cargo.toml`
and the app's entry in `Cargo.lock`. CI runs this check.
- `bun tools/desktop-version.ts --set 0.1.1` writes a new version into all of them.
- `src-tauri/tests/d5.rs` checks the same thing again from the Rust side.
- `bun tools/desktop-version.ts` checks the copies: the agent plugin manifests (`.claude-plugin/plugin.json`,
`marketplace.json`, `.codex-plugin/plugin.json`, `gemini-extension.json`), `apps/desktop/package.json`,
`tauri.conf.json`, `Cargo.toml` and the app's entry in `Cargo.lock`. CI runs this check; `src-tauri/tests/d5.rs`
checks the app's copies from the Rust side.
- `bun tools/desktop-version.ts --set 0.8.0` writes a new version into package.json and all of them.
- **Release order** (one commit, one version): build and check the desktop artifacts (`mac`, `add`), `publish` them
(it refuses a folder whose version is not package.json's), then `npm publish` of the same version (CONTRIBUTING.md).
The app goes first, so `npx storebound desktop` of the new CLI finds its own version.
## Keys and private files (outside every repository)

View file

@ -46,7 +46,8 @@ Types of the protocol: `cli/lib/desktop-protocol.ts`; pure portfolio logic: `cli
- 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.
written into the machine's credential profiles (`~/.storebound/credentials`, files 0600 in folders 0700, the same
store the CLI reads: `profiles.rs`), never shown again and passed to the sidecar per call in memory only.
## Screens and data (D2)
@ -64,7 +65,8 @@ Types of the protocol: `cli/lib/desktop-protocol.ts`; pure portfolio logic: `cli
(`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.
confirmed in a native dialog), store keys into the shared credential profiles 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
@ -104,7 +106,8 @@ Types of the protocol: `cli/lib/desktop-protocol.ts`; pure portfolio logic: `cli
- 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.
- Keys live in the credential profiles shared with the CLI. The state file keeps labels, studio ids and the profile
name of each account only (see "One product with the CLI" below).
- `storebound apps check` gives each account's health, with the fix in words:
- ok;
- no-apps;
@ -159,7 +162,8 @@ host state file (`overrides_set`, every key checked in `workspace.rs`).
- 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.
- Reset: native confirmation, then every saved state of the app is removed; the shared credential profiles and the
app folder list stay (the CLI uses them).
### Writes in the window
@ -197,12 +201,12 @@ Each write is a dry run, then the native dialog, then the real run, with one pro
### 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
(by credential profile name, the same profiles the app uses), 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).
workspace file (each account keeps its profile; accounts of older apps get a profile name to create);
`storebound ws desktop export --merge --apply` adds a workspace to `state.json` while the app is closed (backup kept,
nothing removed; accounts carry their profile, so their keys are already there). Project ids in an exported state are stable per folder path (16 hex).
### Fill in console
@ -337,6 +341,45 @@ Download sources (one click on the Dependencies screen or in the first-run guide
- **Progress and cancel** go through the same progress events as sidecar jobs; a cancelled download removes its
temporary file.
## One product with the CLI (0.8)
The CLI and the app are one product: one version number, one set of store accounts, one list of app folders.
- **One version.** `package.json`'s `version` is the app's too (`tools/desktop-version.ts` keeps the copies equal,
CI and `src-tauri/tests/d5.rs` check it), and both are released together (docs/desktop-release.md). The app runs
its bundled CLI (the sidecar), which is that same version. A folder whose `package.json` pins another storebound
(`devDependencies.storebound`) gets a notice on its app page: "This project uses storebound X, the app runs Y"
(`workspace::pin_differs`; tags, ranges and `file:` specs give none).
- **`npx storebound desktop`** (cli/desktop.ts, the plans in cli/lib/desktop-install.ts): the app has the CLI's
version: open it on the current folder. Not installed: download it for this OS (the CLI's own version from
`storebound.dev/desktop/dl/<version>/latest.json`, else the channel's newest), check size, SHA-256 and the minisign
signature, install it for this user without admin rights (macOS: the .app from the read-only mounted DMG into
/Applications when writable, else ~/Applications, no extended attributes touched; Windows: the NSIS installer with
`/S`, per user; Linux: the AppImage into ~/.local/bin plus `~/.local/share/applications/storebound.desktop` with
the `storebound://` handler, registered with `update-desktop-database` / `xdg-mime` when they exist; a .deb is only
printed, it needs root), then open it. Older app: the same path as an update (never while the app runs); newer
app: opened with a note, a downgrade only with `--force`. In a terminal it asks once (`[Y/n]`), `--yes` skips the
question, without a terminal and `--yes` it prints the command and exits 3. `--dry` and `--json` change nothing;
`--download` only saves the checked installer. `storebound-desktop --version` prints the version (Windows, Linux).
- **Accounts** live in the machine's credential profiles, `~/.storebound/credentials/<name>.json` with key files in
`<name>/` (0600, folders 0700; `src-tauri/src/profiles.rs`, the CLI's cli/lib/credentials.ts and cli/lib/accounts.ts).
An app account is one store of one profile (`Account.profile`); its id for a profile the CLI made is the CLI
workspace's (`sha256("<store>:<profile>")`, 16 hex). Adding a key in the app writes the profile that already has
that key (same key id / service account e-mail), else one named after the label; removing one moves its key file to
`credentials/.removed/`. At start and whenever the list is shown, `profiles::sync` adds an account for every
profile store the app does not have yet. **Migration:** accounts of earlier versions kept their keys in the OS
keychain (`dev.storebound.desktop`); the first start of 0.8 writes each into a profile (keeping the account's id,
label, studio and app mappings), reads it back and only then deletes the keychain entry. An account whose keychain
entry is missing stays, marked "key not moved from the keychain yet" (host.log says which).
Why files and not the keychain: the CLI has no runtime dependencies and runs on three systems and in agents'
terminals; profile files were already its model, so one store means the app writes those files (owner-only modes,
like `~/.ssh` and `~/.appstoreconnect/private_keys`) instead of the CLI learning three keychain APIs.
- **App folders** are the machine's list `~/.storebound/projects.json` (`src-tauri/src/registry.rs`, the CLI's
cli/lib/registry.ts): `storebound desktop` in an app folder, `storebound apps add`, `ws init` and the App folders
screen add to it; `storebound apps folders` lists it; `storebound ws` without a workspace file works over it. The
app keeps its per-folder extras (accounts) in `state.json` under the same id and syncs at start, when the list is
shown and before a `storebound://open` link (a folder already on the list is selected without a question).
## Release, updates and the terminal bridge (D5)
- Releases: [desktop-release.md](desktop-release.md) (version authority, privacy gate, macOS signing and
@ -344,9 +387,10 @@ Download sources (one click on the Dependencies screen or in the first-run guide
- **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,
- **`storebound://open?path=<absolute folder>`** (sent by `storebound desktop` in an app folder, which first puts
the folder on the machine's list): the host accepts one plain absolute path (`policy::deep_link_open_path`), syncs
the list, selects that app folder when it is on it, 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.
@ -368,7 +412,7 @@ Download sources (one click on the Dependencies screen or in the first-run guide
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
`state.json.reset-<time>.bak` after a native confirmation; keys stay in `~/.storebound/credentials`). `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`,

View file

@ -1,5 +1,11 @@
# Getting started
**The model.** storebound is installed once per machine (`npx storebound` / the desktop app, one version number).
Each project has its `storebound.config.ts` and a gitignored `.storebound/` folder (upload keystore, review contact
and demo account). Store accounts live once per machine in `~/.storebound/credentials` (shared by CLI and app:
`storebound accounts`), and so does the list of app folders (`~/.storebound/projects.json`: `storebound apps folders`,
the desktop's App folders). `npx storebound desktop` installs the app (asks once) and opens it on the current folder.
## 5-minute quick start
You need [Node](https://nodejs.org) 20+ or [Bun](https://bun.sh) 1.1+ (and `ffmpeg` for icons, screenshots and video).
@ -75,7 +81,22 @@ the age rating config, lanes and signing (`match`) are listed with their storebo
## Credentials (never in the repo)
Read from environment variables, or from `.storebound/credentials.json` in your project (gitignored by `init`):
Store accounts belong to the machine, everything else to the project:
- **Store accounts** (App Store Connect team key, Play service account, vendor number): a credential profile in
`~/.storebound/credentials/<name>.json`, its key files next to it in `<name>/` (mode 0600). The desktop app adds and
reads the same profiles. Already have the keys in a project's `.storebound/credentials.json`? Move them once:
`npx storebound accounts import --dry`, then `npx storebound accounts import --yes` in every project: the same key
id or service account is one profile (`default` for the first one), and the project file keeps the rest and gets
`"account": "<name>"`. `npx storebound accounts` lists the profiles and which one this folder uses.
- **Which profile:** `--account <name>` for one run, `account: '<name>'` in the config, `"account"` in the project's
`.storebound/credentials.json`, else the only profile on the machine (or the one named `default`).
- **Per project:** the Android upload keystore, the App Review contact and demo account stay in the project's
`.storebound/credentials.json`.
- **Order:** environment variables win over everything, then a profile the project names, then the project file,
then the default profile.
The names (env, profile and project file alike; the project file is gitignored by `init`):
| name | what |
|---|---|
@ -87,9 +108,8 @@ Read from environment variables, or from `.storebound/credentials.json` in your
| `reviewContact` | (file only) App Review contact name / phone / email |
| `reviewDemoAccount` / `REVIEW_DEMO_USER`, `REVIEW_DEMO_PASSWORD` | demo account for App Review (`reviewAccess.signInRequired`); never in the config or the repo, masked in logs |
Several store accounts on one machine: credential profiles (`~/.storebound/credentials/<profile>.json`, the same names
except the per-app upload keystore)
picked with `--account <profile>` or by a workspace ([Many apps and studios](studios.md#many-apps-and-studios)).
Several store accounts on one machine: one profile each, picked as above or by a workspace
([Many apps and studios](studios.md#many-apps-and-studios)).
## Install for your AI tool

View file

@ -48,10 +48,11 @@ a folder takes the store apps its config names, the remaining iOS and Android ap
`unpairs` keep two apart, and the rest are store-only apps that `ws status` reads too.
**The desktop app** keeps the same model (studios, accounts, folders, overrides). `ws desktop import` turns its
`state.json` (or `--state <file>`) into a workspace file and lists the credential profiles to create: the desktop's
keychain is never read. `ws desktop export` prints the workspace as a desktop state (`--out <file>`), and
`--merge --apply` adds it to the desktop's `state.json` while the app is closed (a backup is kept; nothing is
removed; new accounts get their keys in the desktop's Accounts screen).
`state.json` (or `--state <file>`) into a workspace file; its accounts name the credential profiles the app and the
CLI share (`~/.storebound/credentials`), so the keys are already there. `ws desktop export` prints the workspace as a
desktop state (`--out <file>`), and `--merge --apply` adds it to the desktop's `state.json` while the app is closed (a
backup is kept; nothing is removed). Without a workspace file, `ws` works over the machine's app folder list
(`~/.storebound/projects.json`, the desktop's App folders).
MCP: `workspace_list` (offline) and `workspace_status` (read-only).