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:
parent
bba46b5c7f
commit
d2ce185441
60 changed files with 2443 additions and 246 deletions
|
|
@ -1,5 +1,8 @@
|
|||
// `storebound ws`: many apps and studios from one folder (storebound.workspace.json; model: cli/lib/workspace.ts).
|
||||
// ws init [--dry] [--force] find the app folders below and write storebound.workspace.json
|
||||
// (they join this machine's app folder list too)
|
||||
// Without a storebound.workspace.json in this folder or above, ws works over this machine's app folders
|
||||
// (~/.storebound/projects.json: `storebound apps folders`, the desktop app's list).
|
||||
// ws list [--all] [--json] apps (folders + store-only apps from `ws apps import`), studios, accounts
|
||||
// ws status [--offline] [--apps a,b] [--json] portfolio table: apps x stores (release trains, catalog, check), read
|
||||
// in parallel (concurrency from the workspace, default 4)
|
||||
|
|
@ -15,7 +18,7 @@
|
|||
// Credentials: per account a credential profile (cli/lib/credentials.ts), handed to each app's child command as env.
|
||||
// Nothing here prints a secret.
|
||||
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { homedir, tmpdir } from 'node:os';
|
||||
import { join, relative, resolve, sep } from 'node:path';
|
||||
import type { Args } from './lib/args';
|
||||
import { emptyStatus, fillAppStatus, lastLine, pool } from './lib/app-status';
|
||||
|
|
@ -29,9 +32,10 @@ import { textTable } from './lib/table';
|
|||
import { laneOf, laneText } from './lib/train';
|
||||
import {
|
||||
type Account, accountsOf, type AppFolder, cacheFile, type DesktopState, desktopStateFile, fromDesktopState, type LoadedWorkspace, loadWorkspace, mergeDesktopState,
|
||||
overridesOf, readStoreApps, resolveApps, selectApps, type Store, type StoreAppsCache, studiosOf, toDesktopState, type Workspace, WORKSPACE_NAMES,
|
||||
findWorkspace, overridesOf, readStoreApps, resolveApps, selectApps, type Store, type StoreAppsCache, studiosOf, toDesktopState, type Workspace, WORKSPACE_NAMES,
|
||||
} from './lib/workspace';
|
||||
import { FORBIDDEN_ARGS, TOOLS } from './mcp-tools';
|
||||
import { addFolder, readRegistry, registryFile } from './lib/registry';
|
||||
import { checkVirtual, VIRTUAL_READS, virtualDir } from './serve';
|
||||
|
||||
/** One app of the workspace: a folder or a store-only app, with the credentials env its commands get. */
|
||||
|
|
@ -44,7 +48,9 @@ const USAGE = `usage: storebound ws <init | list | status | check | validate | r
|
|||
|
||||
/** Credentials env for one app: the apple account's ASC_* and the play account's PLAY_* from their profiles. */
|
||||
export function appEnv(u: UnifiedApp<AppFolder>, accounts: Account[]): { env: Record<string, string>; problems: string[] } {
|
||||
const env: Record<string, string> = { STOREBOUND_PROFILE: '' };
|
||||
// with workspace accounts the child gets exactly their keys (no other profile); without, the app's own choice
|
||||
// (config `account`, its .storebound "account", the machine's default profile) applies as in a terminal
|
||||
const env: Record<string, string> = {};
|
||||
const problems: string[] = [];
|
||||
const profileOf = (store: Store): string | null => {
|
||||
const fromFolder = store === 'apple' ? u.project?.appleProfile : u.project?.playProfile;
|
||||
|
|
@ -62,6 +68,7 @@ export function appEnv(u: UnifiedApp<AppFolder>, accounts: Account[]): { env: Re
|
|||
for (const store of ['apple', 'play'] as const) {
|
||||
const p = profileOf(store);
|
||||
if (!p) continue;
|
||||
env.STOREBOUND_PROFILE = '';
|
||||
try {
|
||||
const all = profileEnv(p);
|
||||
const keys = store === 'apple' ? ['ASC_KEY_ID', 'ASC_ISSUER_ID', 'ASC_P8'] : ['PLAY_SERVICE_ACCOUNT'];
|
||||
|
|
@ -146,8 +153,13 @@ const catalogCell = (s: AppStatus): string => {
|
|||
return `${c.products + c.subscriptions} items${c.checked === 'live' ? (issues ? `, ${issues} mismatched` : ', in parity') : ''}`;
|
||||
};
|
||||
|
||||
/** The workspace file in this folder or above; without one, the app folders of this machine (cli/lib/registry.ts:
|
||||
* the desktop app's list too), each with its own account choice. */
|
||||
async function loadAll(args: Args): Promise<LoadedWorkspace> {
|
||||
return loadWorkspace(process.cwd(), args.get('workspace'));
|
||||
if (args.get('workspace') || findWorkspace(process.cwd())) return loadWorkspace(process.cwd(), args.get('workspace'));
|
||||
const reg = readRegistry();
|
||||
if (!reg.projects.length) return loadWorkspace(process.cwd());
|
||||
return { file: registryFile(), root: homedir(), ws: { apps: reg.projects.map(p => ({ path: p.path, ...(p.name ? { name: p.name } : {}) })) } };
|
||||
}
|
||||
|
||||
function concurrencyOf(args: Args, ws: Workspace): number {
|
||||
|
|
@ -220,6 +232,10 @@ async function init(args: Args): Promise<void> {
|
|||
const giText = existsSync(gi) ? readFileSync(gi, 'utf8') : '';
|
||||
if (!/^\.storebound\/?$/m.test(giText)) writeFileSync(gi, giText + (giText && !giText.endsWith('\n') ? '\n' : '') + '.storebound/\n');
|
||||
log('ws', `wrote ${file}: ${apps.length ? apps.join(', ') : 'no app folders found yet (add them under "apps")'}`);
|
||||
// the folders also join this machine's list (storebound apps folders, the desktop app)
|
||||
const found = resolveApps({ file, root, ws }).apps;
|
||||
for (const f of found) addFolder(f.path);
|
||||
if (found.length) log('ws', `${found.length} folder(s) in this machine's app list too (${registryFile().replace(homedir(), '~')})`);
|
||||
log('ws', 'next: add studios and accounts by credential profile (~/.storebound/credentials/<profile>.json), then storebound ws list');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue