// What this machine has for storebound, and how to get what is missing. The same detection as the desktop app's // 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] import { existsSync } from 'node:fs'; import { join, resolve } from 'node:path'; import pkg from '../package.json' with { type: 'json' }; import type { Args } from './lib/args'; import { CONFIG_NAMES } from './lib/config'; import { credentialStatus } from './lib/credentials'; import { log } from './lib/log'; import { textTable } from './lib/table'; import { androidSdk, type AndroidSdkLookup, detectTools, findPlaywright, installCommands, type ToolStatus } from './lib/tools'; import * as rt from './lib/rt'; import { foreignRsync } from './lib/xcode'; export interface DoctorRow { id: ToolStatus['id'] | 'playwright'; name: string; found: boolean; version: string | null; path: string | null; source: ToolStatus['source']; usedFor: string; /** Install commands for this OS (empty when found). */ install: string[]; url: string | null; } export interface DoctorResult { storebound: string; bun: string | null; runtime: string; platform: string; project: string | null; tools: DoctorRow[]; missing: number; /** The Android SDK `play build` hands to Gradle, where it came from, and every place looked at. */ androidSdk: AndroidSdkLookup; /** Store credentials of the project folder (env, .storebound/credentials.json, profile): set, missing or * placeholder (a template value, treated as missing). Never the values. */ credentials: { key: string; status: 'set' | 'missing' | 'placeholder' }[]; /** macOS: a non-Apple rsync first on PATH (Homebrew 3.x breaks Xcode's exportArchive, pitfall 54), else null. */ foreignRsync: string | null; } export function doctor(cwd: string, env: Record = 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 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), foreignRsync: process.platform === 'darwin' ? foreignRsync(env.PATH) : null }; } export async function run(args: Args): Promise { const r = doctor(resolve(args.str('dir', '.'))); if (args.has('json')) { console.log(JSON.stringify(r, null, 2)); 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]))); 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); 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):'); for (const t of missing) { console.log(` ${t.name}`); for (const c of t.install) console.log(` ${c}`); if (t.url) console.log(` ${t.url}`); } log('doctor', `${missing.length} missing. Store work needs none of them; screenshots, previews, icons and captures do.`); }