- 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
65 lines
4.9 KiB
TypeScript
65 lines
4.9 KiB
TypeScript
// `storebound accounts`: the store accounts of this machine, shared by every project, the CLI and the desktop app
|
|
// (credential profiles in ~/.storebound/credentials; model: cli/lib/credentials.ts, cli/lib/accounts.ts). Values
|
|
// are never printed.
|
|
// storebound accounts [list] [--json] every profile: Apple key, Play account (masked), which
|
|
// one this folder uses and why
|
|
// storebound accounts import [--dir <project>] [--name <profile>] [--dry] [--yes]
|
|
// move the store keys of a project's .storebound/
|
|
// credentials.json into a profile (the same account as an
|
|
// existing profile = that profile); the keystore, review
|
|
// contact and demo account stay in the project
|
|
// storebound accounts use <profile> [--dir <project>] this project uses that profile ("account" in its
|
|
// .storebound/credentials.json)
|
|
import { resolve } from 'node:path';
|
|
import type { Args } from './lib/args';
|
|
import { applyImport, describeProfiles, planImport } from './lib/accounts';
|
|
import { activeProfile, profileDir } from './lib/credentials';
|
|
import { confirm, type Ctx } from './lib/ctx';
|
|
import { textTable } from './lib/table';
|
|
|
|
const tilde = (p: string) => (process.env.HOME && p.startsWith(process.env.HOME) ? '~' + p.slice(process.env.HOME.length) : p);
|
|
|
|
export async function run(args: Args): Promise<void> {
|
|
const sub = args._[0] ?? 'list';
|
|
const dir = resolve(args.str('dir', '.'));
|
|
if (sub === 'list') {
|
|
const rows = describeProfiles();
|
|
let active = null;
|
|
try { active = activeProfile(dir); } catch { /* unreadable project file */ }
|
|
if (args.has('json')) { console.log(JSON.stringify({ folder: tilde(profileDir()), profiles: rows, thisFolder: active }, null, 2)); return; }
|
|
if (!rows.length) {
|
|
console.log(`no store accounts on this machine yet (${tilde(profileDir())}).\n From a project that has its keys in .storebound/credentials.json: storebound accounts import\n Or add them in the desktop app (Accounts).`);
|
|
return;
|
|
}
|
|
console.log(textTable(['profile', 'App Store Connect', 'Google Play', 'vendor no.'], rows.map(r => [r.profile, r.error ? `unreadable: ${r.error}` : r.apple ? 'key' : '-', r.play ? r.playAccount ?? 'service account' : '-', r.vendor ? 'yes' : '-'])));
|
|
console.log(`\n${tilde(profileDir())}: shared by the CLI and the desktop app`);
|
|
console.log(active ? `this folder uses "${active.name}" (${{ env: '--account / STOREBOUND_PROFILE', config: 'account in the config', project: '"account" in .storebound/credentials.json', default: 'the default profile' }[active.from]})` : 'this folder uses no profile (several profiles and none named "default": storebound accounts use <profile>)');
|
|
return;
|
|
}
|
|
if (sub === 'import') {
|
|
const plan = planImport(dir, { name: args.get('name') });
|
|
const json = args.has('json');
|
|
const dry = args.has('dry');
|
|
if (json && dry) { console.log(JSON.stringify({ ...plan, writes: plan.writes.map(w => ({ path: tilde(w.path), what: w.what, action: w.action })), project: tilde(plan.project), projectFile: tilde(plan.projectFile) }, null, 2)); return; }
|
|
console.log(`${dry ? 'dry run: ' : ''}import the store accounts of ${tilde(plan.project)} into profile "${plan.profile}"${plan.reuse ? ' (the same account is already there)' : ' (new)'}`);
|
|
console.log(` stores: ${plan.stores.join(', ')}; moved out of the project file: ${plan.moved.join(', ')}`);
|
|
for (const w of plan.writes) console.log(` ${w.action === 'same' ? 'unchanged' : w.action.padEnd(9)} ${tilde(w.path)} (${w.what}${w.mode === 0o600 && w.action !== 'same' ? ', mode 600' : ''})`);
|
|
for (const n of plan.notes) console.log(` note: ${n}`);
|
|
console.log(' stays in the project: the Android upload keystore, the review contact and demo account, everything else');
|
|
if (dry) { console.log('\nnothing written. Run again without --dry to import.'); return; }
|
|
if (plan.writes.every(w => w.action === 'same')) { console.log('\nalready imported: nothing to change'); return; }
|
|
if (!(await confirm({ args } as unknown as Ctx, `Import into profile "${plan.profile}"?`))) return;
|
|
const r = applyImport(plan);
|
|
console.log(`\nimported: profile "${plan.profile}"${r.backup ? `; the old project file is kept as ${tilde(r.backup)}` : ''}`);
|
|
return;
|
|
}
|
|
if (sub === 'use') {
|
|
const name = args._[1];
|
|
if (!name) throw new Error('storebound accounts use <profile>');
|
|
const { useProfile } = await import('./lib/accounts');
|
|
const f = useProfile(dir, name);
|
|
console.log(`${tilde(f)}: "account": "${name}"`);
|
|
return;
|
|
}
|
|
throw new Error('usage: storebound accounts [list | import [--dir <project>] [--name <profile>] [--dry] [--yes] | use <profile>]');
|
|
}
|