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

@ -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`,