doctor: build dependencies (JDK with the version the project needs, Android SDK command-line tools, CocoaPods, Node, Bun, bundletool), too-old status, doctor --install <tool> --yes runs the package-manager command (never sudo); desktop Dependencies shows too old; gen-flags works on Windows paths
This commit is contained in:
parent
a8a889fb35
commit
2598c4953a
10 changed files with 137 additions and 14 deletions
|
|
@ -2,6 +2,8 @@
|
|||
// Dependencies screen (lib/tools.ts). Store work (listings, products, releases, reviews) needs none of these tools;
|
||||
// screenshots, app previews, icons and simulator / emulator captures do. Read-only: it prints commands, it runs none.
|
||||
// storebound doctor [--dir .] [--json]
|
||||
// storebound doctor --install <tool> [--yes] runs this OS's install command for one tool (winget / brew / apt);
|
||||
// without --yes only prints it. Never sudo, never an admin prompt.
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import pkg from '../package.json' with { type: 'json' };
|
||||
|
|
@ -22,6 +24,9 @@ export interface DoctorRow {
|
|||
path: string | null;
|
||||
source: ToolStatus['source'];
|
||||
usedFor: string;
|
||||
/** The version the project needs and ok / missing / outdated. */
|
||||
required?: string | null;
|
||||
status?: 'ok' | 'missing' | 'outdated';
|
||||
/** Install commands for this OS (empty when found). */
|
||||
install: string[];
|
||||
url: string | null;
|
||||
|
|
@ -38,30 +43,54 @@ export interface DoctorResult {
|
|||
}
|
||||
|
||||
export function doctor(cwd: string, env: Record<string, string | undefined> = process.env): DoctorResult {
|
||||
const tools: DoctorRow[] = detectTools(env).map(t => ({ id: t.id, name: t.name, found: t.found, version: t.version, path: t.path, source: t.source, usedFor: t.usedFor,
|
||||
install: t.found ? [] : installCommands(t.id), url: t.install.url }));
|
||||
const tools: DoctorRow[] = detectTools(env, cwd).map(t => ({ id: t.id, name: t.name, found: t.found, version: t.version, path: t.path, source: t.source, usedFor: t.usedFor,
|
||||
required: t.required ?? null, status: t.status, install: t.status === 'ok' ? [] : installCommands(t.id), url: t.install.url }));
|
||||
const pw = findPlaywright([cwd, rt.packageRoot()]);
|
||||
tools.splice(3, 0, { id: 'playwright', name: 'playwright', found: !!pw, version: null, path: pw, source: pw ? 'system' : null,
|
||||
usedFor: 'screenshot renders and web captures from a terminal (the desktop app brings its own)', install: pw ? [] : installCommands('playwright'), url: 'https://playwright.dev/docs/intro' });
|
||||
const project = CONFIG_NAMES.some(n => existsSync(join(cwd, n))) ? cwd : null;
|
||||
const sdk = androidSdk([join(cwd, 'android'), cwd], env);
|
||||
return { storebound: pkg.version, bun: rt.bunVersion, runtime: `${rt.runtime.name} ${rt.runtime.version}`, platform: `${process.platform}-${process.arch}`, project, tools, missing: tools.filter(t => !t.found).length, androidSdk: sdk, credentials: credentialStatus(cwd),
|
||||
return { storebound: pkg.version, bun: rt.bunVersion, runtime: `${rt.runtime.name} ${rt.runtime.version}`, platform: `${process.platform}-${process.arch}`, project, tools, missing: tools.filter(t => !t.found || t.status === 'outdated').length, androidSdk: sdk, credentials: credentialStatus(cwd),
|
||||
foreignRsync: process.platform === 'darwin' ? foreignRsync(env.PATH) : null };
|
||||
}
|
||||
|
||||
/** One install command of a tool, run only with --yes; sudo / admin commands are printed, never run. */
|
||||
export function installPlan(r: DoctorResult, id: string, platform: NodeJS.Platform = process.platform): { tool: DoctorRow; command: string | null; runnable: boolean } {
|
||||
const tool = r.tools.find(t => t.id === id);
|
||||
if (!tool) throw new Error(`doctor --install: no tool "${id}" (one of ${r.tools.map(t => t.id).join(', ')})`);
|
||||
const command = installCommands(tool.id, platform)[0] ?? null;
|
||||
return { tool, command, runnable: !!command && !/^sudo |\| *(ba)?sh|^Install /.test(command) };
|
||||
}
|
||||
|
||||
export async function run(args: Args): Promise<void> {
|
||||
const r = doctor(resolve(args.str('dir', '.')));
|
||||
const want = args.get('install');
|
||||
if (want) {
|
||||
const plan = installPlan(r, want);
|
||||
if (plan.tool.status === 'ok') { log('doctor', `${plan.tool.name} ${plan.tool.version ?? ''} is already here${plan.tool.required ? ` (the project needs ${plan.tool.required})` : ''}`); return; }
|
||||
if (!plan.command) throw new Error(`doctor --install: no install command for ${plan.tool.name} on this OS: ${plan.tool.url}`);
|
||||
if (!plan.runnable || !args.has('yes')) {
|
||||
log('doctor', `would run: ${plan.command}${plan.runnable ? ' (add --yes to run it)' : ' (needs admin rights: run it yourself)'}`);
|
||||
return;
|
||||
}
|
||||
log('doctor', `installing ${plan.tool.name}: ${plan.command}`);
|
||||
const p = rt.spawnSync(plan.command.split(/\s+/), { stdout: 'inherit', stderr: 'inherit', timeout: 20 * 60_000 });
|
||||
if (!p.success) throw new Error(`${plan.command} failed (exit ${p.exitCode}); open a new terminal if it installed but is not on PATH yet`);
|
||||
const after = doctor(resolve(args.str('dir', '.'))).tools.find(t => t.id === plan.tool.id);
|
||||
log('doctor', `${plan.tool.name}: ${after?.status ?? 'unknown'}${after?.version ? ` ${after.version}` : ''} (a new terminal may be needed for PATH)`);
|
||||
return;
|
||||
}
|
||||
if (args.has('json')) { printJson(r); return; }
|
||||
log('doctor', `storebound ${r.storebound} · ${r.runtime} · ${r.platform}${r.project ? ' · project config found' : ''}`);
|
||||
console.log(textTable(['tool', 'status', 'version', 'used for'], r.tools.map(t => [t.name, t.found ? (t.source === 'app' ? 'ok (app copy)' : 'ok') : 'missing', t.version ?? '', t.usedFor])));
|
||||
console.log(textTable(['tool', 'status', 'version', 'needs', 'used for'], r.tools.map(t => [t.name, t.status === 'outdated' ? 'too old' : t.found ? (t.source === 'app' ? 'ok (app copy)' : 'ok') : 'missing', t.version ?? '', t.required ?? '', t.usedFor])));
|
||||
log('doctor', r.androidSdk.path ? `Android SDK: ${r.androidSdk.path} (from ${r.androidSdk.source})`
|
||||
: `Android SDK: not found (play build needs it). Checked: ${r.androidSdk.checked.join('; ')}`);
|
||||
const set = r.credentials.filter(c => c.status === 'set').map(c => c.key), ph = r.credentials.filter(c => c.status === 'placeholder').map(c => c.key);
|
||||
log('doctor', `credentials: ${set.length ? set.join(', ') : 'none set'}${ph.length ? ` · placeholders (treated as missing): ${ph.join(', ')}` : ''}`);
|
||||
if (r.foreignRsync) log('doctor', `rsync: ${r.foreignRsync} is first on PATH and breaks Xcode exportArchive ("Copy failed") outside storebound; storebound apple build puts /usr/bin first. Fix for everything else: brew unlink rsync, or put /usr/bin before it`);
|
||||
const missing = r.tools.filter(t => !t.found);
|
||||
const missing = r.tools.filter(t => !t.found || t.status === 'outdated');
|
||||
if (!missing.length) { log('doctor', 'everything storebound can use is here'); return; }
|
||||
console.log('\nTo install what is missing (pick one per tool; storebound never runs these for you):');
|
||||
console.log('\nTo install what is missing (storebound doctor --install <tool> --yes runs the first one; sudo / admin ones you run yourself):');
|
||||
for (const t of missing) {
|
||||
console.log(` ${t.name}`);
|
||||
for (const c of t.install) console.log(` ${c}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue