storebound/cli/accounts.ts

94 lines
7.7 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)
// storebound accounts play-developer <profile> <id> | --clear
// the profile's Play developer account id (the digits in
// play.google.com/console/u/0/developers/<id>): Play
// Console links open the app's own page
// storebound accounts default <profile> | --clear the machine's default profile (a pointer file in the
// credentials folder). Order: env > a profile the project
// names > the project's own keys > this default > the only
// profile (or the one named "default")
import { resolve } from 'node:path';
import type { Args } from './lib/args';
import { applyImport, describeProfiles, planImport, setPlayDeveloperId } from './lib/accounts';
import { activeProfile, defaultPointer, defaultProfile, profileDir, setDefaultPointer } from './lib/credentials';
import { confirm, type Ctx } from './lib/ctx';
import { textTable } from './lib/table';
import { printJson } from './lib/log';
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 */ }
const def = defaultProfile();
const pointer = defaultPointer();
if (args.has('json')) { printJson({ folder: tilde(profileDir()), profiles: rows.map(r => ({ ...r, default: r.profile === def })), default: def, defaultFrom: def ? (def === pointer ? 'pointer' : rows.length === 1 ? 'only' : 'named-default') : null, thisFolder: active }); 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', 'Play developer id', 'vendor no.', 'default'], rows.map(r => [r.profile, r.error ? `unreadable: ${r.error}` : r.apple ? 'key' : '-', r.play ? r.playAccount ?? 'service account' : '-', r.playDeveloperId ?? '-', r.vendor ? 'yes' : '-', r.profile === def ? (def === pointer ? 'yes (accounts default)' : 'yes') : ''])));
if (pointer && pointer !== def) console.log(`note: the default pointer names "${pointer}", which is not a profile here (storebound accounts default <profile>)`);
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 no default: storebound accounts default <profile>, or accounts use <profile> in the project)');
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) { printJson({ ...plan, writes: plan.writes.map(w => ({ path: tilde(w.path), what: w.what, action: w.action })), project: tilde(plan.project), projectFile: tilde(plan.projectFile) }); 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;
}
if (sub === 'play-developer') {
const name = args._[1];
if (!name) throw new Error('storebound accounts play-developer <profile> <id> | --clear');
const id = args.has('clear') ? null : args._[2] ?? null;
if (!args.has('clear') && !id) throw new Error('storebound accounts play-developer <profile> <id>: the digits in play.google.com/console/u/0/developers/<id>');
const f = setPlayDeveloperId(name, id);
console.log(id ? `${tilde(f)}: PLAY_DEVELOPER_ID ${id} (Play Console links of this account's apps open the app's page)` : `${tilde(f)}: Play developer id removed`);
return;
}
if (sub === 'default') {
if (args.has('clear')) { setDefaultPointer(null); console.log('default profile pointer removed (default: the only profile, or the one named "default")'); return; }
const name = args._[1];
if (!name) { const d = defaultProfile(); console.log(d ? `default profile: ${d}${d === defaultPointer() ? ' (accounts default)' : ''}` : 'no default profile (storebound accounts default <profile>)'); return; }
const f = setDefaultPointer(name);
console.log(`${tilde(f)}: default profile "${name}" (used by every folder that names no profile and has no keys of its own)`);
return;
}
throw new Error('usage: storebound accounts [list | import [--dir <project>] [--name <profile>] [--dry] [--yes] | use <profile> | play-developer <profile> <id> | default <profile> | default --clear]');
}