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

@ -2,6 +2,44 @@
## Unreleased
One product, one mental model: storebound is installed once per machine (`npx storebound` and the desktop app),
each project has its `storebound.config.ts` and `.storebound/`, and store accounts and app folders live once per
machine in `~/.storebound`, shared by the CLI and the app.
- **One version.** The desktop app has the CLI's version from now on; the next release is 0.8.0 for both.
`package.json`'s `version` is the authority (`desktopVersion` is gone); `tools/desktop-version.ts` keeps the plugin
manifests and the app's copies equal (CI, and `tests/desktop-release.test.ts` / `src-tauri/tests/d5.rs` as the
drift tests), `desktop-release.ts publish` refuses a folder of another version, and the release order is the app
first, then `npm publish`. The desktop 0.1.0 release candidate was never published, so nothing public changes.
- **`npx storebound desktop` just works.** The app has this CLI's version: it opens on the current folder. Not
installed: it downloads the app for this system (the CLI's own version, else the newest published), checks size,
SHA-256 and the minisign signature, installs it for this user without admin rights (macOS: from the read-only DMG
into /Applications or ~/Applications, no xattr changes; Windows: the per-user NSIS installer, silent; Linux: the
AppImage in ~/.local/bin with a `.desktop` entry and the `storebound://` handler) and opens it. An older app is
updated the same way (never while it runs), a newer one is opened with a note (downgrade only with `--force`). It
asks once (`[Y/n]`); `--yes` skips the question; without a terminal and `--yes` it prints the command and exits 3.
`--dry` / `--json` change nothing, `--download` only saves the checked installer. The publish script also writes
`desktop/dl/<version>/latest.json` (redeploy `tools/vds/storebound-desktop-publish` on the server).
- **Store accounts, one store.** Credential profiles in `~/.storebound/credentials` are shared by the CLI and the
desktop app: the app writes its keys there (files 0600, folders 0700) and shows the profiles the CLI made; accounts
of earlier desktop builds move from the OS keychain into profiles on first start (ids, studios and app mappings
kept; the keychain entry is deleted after the profile reads back). A removed account's key file goes to
`credentials/.removed/`. Precedence: env, then a profile the project names, then the project's
`.storebound/credentials.json`, then the default profile (the only one, or `default`). A project names its profile
with `account:` in the config, `"account"` in its `.storebound/credentials.json`, or `--account`.
- **`storebound accounts`** lists the profiles (masked) and which one this folder uses; `accounts import` moves the
store keys of a project's `.storebound/credentials.json` into a profile (key files copied to
`~/.storebound/credentials/<name>/`, the same Apple key id or service account e-mail = the same profile, the
keystore and review contact stay in the project, a backup of the project file is kept, safe to run again; `--dry`
first, never prints values); `accounts use <profile>` points a project at one.
- **App folders, one list.** `~/.storebound/projects.json` is the desktop's App folders and the CLI's
`storebound apps folders | add | remove`; `storebound desktop` in an app folder and `ws init` add to it, and `ws`
without a workspace file works over it. The app syncs it at start, when the list is shown and before a
`storebound://open` link, so a folder added in the terminal is selected without a question.
- **Desktop:** a folder whose `package.json` pins another storebound version shows "This project uses storebound X,
the app runs Y". `storebound-desktop --version`. `ws desktop import / export` carry the accounts' profile names.
- `ws` children get the app's own account choice when the workspace names no account (before: no profile at all).
Fixes from a real first release of a second game (Capacitor 8 monorepo, lessons 35 to 37):
- Fix (`scan`, review-prompt 3.2.2(x)): a bare "rate" next to "bonus" (`const rate = F.genRate(lv, bonus.gen(r))`) is