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

@ -411,7 +411,7 @@ export const en = {
'checks.sev.accepted': 'Accepted',
'checks.sev.low': 'Low',
'folders.lead': 'One folder per app with its storebound.config.ts (iOS and Android in one config). Pick the store accounts each app uses; the keys stay in the keychain.',
'folders.lead': 'One folder per app with its storebound.config.ts (iOS and Android in one config). This list is the machine\'s (~/.storebound/projects.json): `storebound desktop` or `storebound apps add` in a terminal adds to it too. Pick the store accounts each app uses.',
'folders.list': 'App folders',
'folders.listSub': '{count} folders',
'folders.listSub_one': '1 folder',
@ -445,16 +445,19 @@ export const en = {
'run.dryFailed': 'The dry run failed: nothing was written',
'run.done': 'Done',
'accounts.lead': 'Store API keys live in the system keychain (Keychain on macOS, Credential Manager on Windows, Secret Service on Linux). They are never shown again, never written to your app folders, and reach storebound in memory for one call.',
'accounts.lead': 'Store accounts live once per machine in ~/.storebound, shared with the storebound CLI (`storebound accounts`): one credential profile per account, key files readable only by you. They are never shown again, never written to your app folders, and reach storebound in memory for one call.',
'accounts.list': 'Store accounts',
'accounts.listSub': 'Labels only: the keys are in the keychain',
'accounts.listSub': 'Labels and profile names only: the keys are in ~/.storebound',
'accounts.emptyTitle': 'No store accounts yet',
'accounts.emptyText': 'Add a Google Play service account or an App Store Connect API key below. Store reads need them; app folders work without.',
'accounts.unused': 'Not used',
'accounts.remove': 'Remove',
'accounts.removeAria': 'Remove {label} and delete its key from the keychain',
'accounts.removeAria': 'Remove {label}; its key file moves to a .removed folder in ~/.storebound',
'accounts.saved': '"{label}" saved',
'accounts.savedDetail': 'The key is in the system keychain.',
'accounts.savedDetail': 'The key is in a credential profile in ~/.storebound (the CLI uses it too).',
'acct.profile': 'profile {name}',
'acct.noProfile': 'key not moved from the keychain yet',
'app.pinNotice': 'This project uses storebound {pin} (package.json), the app runs {app}. The app works with its own copy; update the project with `bun add -d storebound@{app}` (or npm i -D) to keep both the same.',
'accounts.playSub': 'Service account JSON key',
'accounts.appleSub': 'Team API key (.p8) from Users and Access, Integrations',
'accounts.label': 'Name',
@ -465,7 +468,7 @@ export const en = {
'accounts.issuerId': 'Issuer ID',
'accounts.choosePlay': 'Choose the JSON key',
'accounts.chooseApple': 'Choose the .p8 key',
'accounts.safety': 'Keys are picked in the system file dialog and go straight to the keychain. The window never sees them.',
'accounts.safety': 'Keys are picked in the system file dialog and go straight into ~/.storebound (readable only by you). The window never sees them.',
'deps.lead': 'Local tools for screenshots, app previews, icons and simulator or emulator captures. Store work (listings, products, releases, reviews) needs none of them.',
'deps.checkAgain': 'Check again',
@ -547,7 +550,7 @@ export const en = {
'wz.p2t': 'Safe by design',
'wz.p2s': 'Reads are read-only. Every change shows a dry run and a native confirmation first.',
'wz.p3t': 'Your keys stay yours',
'wz.p3s': 'Store keys live in the system keychain. No account, no telemetry.',
'wz.p3s': 'Store keys live in ~/.storebound, shared with the storebound CLI. No account, no telemetry.',
'wz.appsTitle': 'Add your apps',
'wz.appsLead': 'Each app is a folder with a storebound.config.ts that describes it on both stores.',
'wz.addTitle': 'Add an existing folder',
@ -607,11 +610,11 @@ export const en = {
'color.teal': 'Teal',
'color.sky': 'Sky',
'color.slate': 'Slate',
'accounts.leadStudios': 'Studios group your store accounts: Google Play service accounts and App Store Connect team keys. Keys stay in the system keychain; apps come in with Import apps.',
'accounts.leadStudios': 'Studios group your store accounts: Google Play service accounts and App Store Connect team keys. Keys live in ~/.storebound, shared with the CLI; apps come in with Import apps.',
'accounts.emptyTextStudios': 'Add a studio, then its Google Play service account and App Store Connect keys. Import apps then lists every app those keys can see.',
'accounts.playSubMany': 'Service account JSON key. One service account can be invited to several Play developer accounts: it sees all their apps.',
'accounts.appleSubMany': 'One API key per team (company or individual): key id, issuer id and the .p8 file.',
'accounts.safetyStudios': 'Keys are read once from the file you pick, stored in the system keychain and never shown again. storebound gets one account\'s key in memory for one call. Studio names, labels and app lists stay on this computer.',
'accounts.safetyStudios': 'Keys are read once from the file you pick, stored in ~/.storebound (readable only by you, shared with the storebound CLI) and never shown again. storebound gets one account\'s key in memory for one call. Studio names, labels and app lists stay on this computer.',
'acct.ok': 'Works',
'acct.noApps': 'No apps',
'acct.permission': 'Permission missing',
@ -768,7 +771,7 @@ export const en = {
'set.writesText': 'Dry run first, then your Apply in a system dialog. Submit, release and publish stay in the consoles.',
'set.privacy': 'Privacy',
'set.p1': 'No telemetry, no analytics, no crash reports: the app talks only to the stores, with your keys, when you ask.',
'set.p2': 'Store keys live in the system keychain; the app data folder holds names, settings and saved reads, never keys.',
'set.p2': 'Store keys live in ~/.storebound and the app folder list in ~/.storebound/projects.json, shared with the CLI; the app data folder holds labels, settings and saved reads, never keys.',
'set.p3': 'Nothing is written into your app folders unless you confirm it in a system dialog.',
'set.p4': 'Tester emails stay masked; review drafts stay in each app folder\'s .storebound/ (gitignored).',
'set.reveal': 'Show the data folder',
@ -785,7 +788,7 @@ export const en = {
'set.updateInstall': 'Install {version}…',
'set.updateInstalling': 'Downloading the update… {done}',
'set.reset': 'Reset',
'set.resetText': 'Forgets {accounts} accounts (their keys are removed from the keychain), {studios} studios, {folders} app folders (the folders stay on disk), pairing, the ignore list, settings and saved reads. A system dialog asks first.',
'set.resetText': 'Forgets {studios} studios, the labels of {accounts} accounts, pairing, the ignore list, settings and saved reads. The accounts in ~/.storebound and the {folders} app folders stay: the CLI uses them too. A system dialog asks first.',
'set.resetButton': 'Reset the app',
'set.resetDone': 'storebound desktop was reset',
'set.footer': '{n} apps known to this computer',
@ -1275,7 +1278,7 @@ export const en = {
'crash.retry': 'Try again',
'crash.reload': 'Reload',
'crash.reset': 'Reset saved state (keeps a backup)',
'crash.resetHint': 'Starts with an empty list. The saved state is kept as a backup file, store keys stay in the keychain, app folders are not touched.',
'crash.resetHint': 'Starts again from the machine\'s accounts and app folders. The saved state is kept as a backup file, store keys stay in ~/.storebound, app folders are not touched.',
'crash.resetDone': 'Saved state reset. Backup: {path}. Reloading…',
'crash.logNote': 'Also written to logs/webview.log in the app data folder (home folder shown as ~, no keys).',
'folder.missing': 'Folder not found.',