Ignore dormant apps: per-app and per-store 'ignored' in the app list; apps ignore/unignore, watch, ws and apps list skip them; MCP tools

This commit is contained in:
CRTkafa 2026-10-23 00:57:42 +08:00
parent 75bf62e125
commit 13b91dd423
7 changed files with 278 additions and 30 deletions

View file

@ -19,7 +19,7 @@ import { CONFIG_NAMES } from './lib/config';
import { consoleIdsFromConfig } from './lib/console-links';
import { profileEnv } from './lib/credentials';
import { done, log, warn } from './lib/log';
import { readRegistry } from './lib/registry';
import { type IgnoreStore, ignoredStores, isIgnored, readRegistry } from './lib/registry';
import { runCommand } from './lib/runner';
import type { HealthSnapshot } from './lib/health';
import {
@ -36,7 +36,7 @@ const MAX_BACKOFF_MS = 4 * 60 * 60_000;
const CHILD_TIMEOUT_MS = 5 * 60_000;
const REVIEW_DAYS = 3;
interface Target extends PassTarget { dir: string; virtual: boolean; env: Record<string, string> }
interface Target extends PassTarget { dir: string; virtual: boolean; env: Record<string, string>; /** store sides the registry ignores: not read, no events */ skip: IgnoreStore[] }
/** Runs a storebound child command; a hung one is cancelled after the timeout. */
const exec = (env: Record<string, string>): Exec => async (argv, cwd) => {
@ -46,26 +46,28 @@ const exec = (env: Record<string, string>): Exec => async (argv, cwd) => {
};
/** The apps to watch: --dir, or the registry's folders and store-only apps (a store-only app that has a folder here is read once). */
async function listTargets(args: Args): Promise<Target[]> {
export async function listTargets(args: Args): Promise<Target[]> {
const dir = args.get('dir');
if (dir) {
const d = resolve(dir);
if (!CONFIG_NAMES.some(n => existsSync(join(d, n)))) throw new Error(`--dir: no storebound.config.ts in ${d}`);
return [{ key: `dir:${d}`, name: d.split(/[\\/]/).pop() ?? d, dir: d, virtual: false, env: {} }];
return [{ key: `dir:${d}`, name: d.split(/[\\/]/).pop() ?? d, dir: d, virtual: false, env: {}, skip: [] }];
}
const reg = readRegistry();
const out: Target[] = [];
for (const p of reg.projects) {
if (isIgnored(p)) continue; // ignored apps are not watched at all (storebound apps ignore / unignore)
if (!CONFIG_NAMES.some(n => existsSync(join(p.path, n)))) { warn(TAG, `${p.name}: no storebound.config.ts in ${p.path}, skipped (storebound apps remove ${p.path})`); continue; }
out.push({ key: `dir:${p.path}`, name: p.name, dir: p.path, virtual: false, env: {} });
out.push({ key: `dir:${p.path}`, name: p.name, dir: p.path, virtual: false, env: {}, skip: ignoredStores(p) });
}
if (reg.storeApps?.length) {
const { checkVirtual, virtualDir } = await import('./serve');
for (const s of reg.storeApps) {
if (isIgnored(s)) continue;
try {
if (s.store === 'apple' && !s.bundleId) throw new Error('no bundle id (storebound apps add --store apple --app-id <id> --bundle-id <id>)');
const v = checkVirtual({ name: s.name, ...(s.store === 'apple' ? { apple: { bundleId: s.bundleId, appId: s.appId } } : { play: { packageName: s.appId } }) });
out.push({ key: `store:${s.id}`, name: s.name, dir: virtualDir(v), virtual: true, env: s.account ? profileEnv(s.account) : {} });
out.push({ key: `store:${s.id}`, name: s.name, dir: virtualDir(v), virtual: true, env: s.account ? profileEnv(s.account) : {}, skip: [] });
} catch (e) { warn(TAG, `${s.name}: ${(e as Error).message}, skipped`); }
}
}
@ -80,24 +82,25 @@ async function readApp(t: Target, prev: AppSnap | undefined, now: number): Promi
const e = emptyStatus(t.dir);
try { await fillAppStatus(e, t.dir, { live: true, virtual: true, exec: run }); } catch (err) { return { snap: null, errors: [(err as Error).message] }; }
const s = e.summary!;
const useApple = !!s.apple && !t.skip.includes('apple'), usePlay = !!s.play && !t.skip.includes('play');
const snap: AppSnap = { name: s.name || t.name, ids: { appleAppId: s.apple?.appId ?? null } };
if (s.play && !t.virtual && !prev?.ids.playDeveloperId) {
if (usePlay && !t.virtual && !prev?.ids.playDeveloperId) {
try { const { loadConfig } = await import('./lib/config'); Object.assign(snap.ids, consoleIdsFromConfig((await loadConfig(undefined, t.dir)).config)); } catch { /* links fall back to the console home */ }
}
const all = e.release.all ?? [];
snap.releases = {};
if (s.apple) { if (e.storeErrors.ios) errors.push(`App Store: ${e.storeErrors.ios}`); else snap.releases.apple = releaseMap(all, 'ios'); }
if (s.play) { if (e.storeErrors.android) errors.push(`Google Play: ${e.storeErrors.android}`); else snap.releases.play = releaseMap(all, 'android'); }
if (useApple) { if (e.storeErrors.ios) errors.push(`App Store: ${e.storeErrors.ios}`); else snap.releases.apple = releaseMap(all, 'ios'); }
if (usePlay) { if (e.storeErrors.android) errors.push(`Google Play: ${e.storeErrors.android}`); else snap.releases.play = releaseMap(all, 'android'); }
const due = !prev?.healthAt || now - Date.parse(prev.healthAt) >= HEALTH_EVERY_MS;
if (due && (s.apple || s.play)) {
if (due && (useApple || usePlay)) {
const h = await run(['health', '--json'], t.dir);
if (h.data && typeof h.data === 'object') { snap.health = healthSnap(h.data as HealthSnapshot); snap.healthAt = new Date(now).toISOString(); }
if (h.data && typeof h.data === 'object') { snap.health = healthSnap({ ...(h.data as HealthSnapshot), ...(useApple ? {} : { apple: undefined }), ...(usePlay ? {} : { play: undefined }) } as HealthSnapshot); snap.healthAt = new Date(now).toISOString(); }
else errors.push(`health: ${lastLine(h.out) || `exit ${h.code}`}`);
}
snap.reviews = {};
for (const [store, on] of [['apple', !!s.apple], ['play', !!s.play]] as const) {
for (const [store, on] of [['apple', useApple], ['play', usePlay]] as const) {
if (!on) continue;
const r = await run(['reviews', 'list', '--store', store, '--days', String(REVIEW_DAYS), '--json'], t.dir);
if (r.code !== 0 || !Array.isArray(r.data)) { errors.push(`reviews ${store}: ${lastLine(r.out) || `exit ${r.code}`}`); continue; }