doctor: warn when storebound runs from a folder macOS guards (a launchd job hangs opening it)

This commit is contained in:
CRTkafa 2026-10-30 00:26:07 +07:00
parent 724f5b9763
commit 152e40fb8a
3 changed files with 28 additions and 1 deletions

View file

@ -3,6 +3,7 @@
## Unreleased
- `doctor` warns when storebound runs from a folder macOS guards (~/Desktop, ~/Documents, ~/Downloads, iCloud Drive; e.g. a `bun link` to a checkout there): a launchd job or CI agent without access to it blocks in open() with no output.
- `apple build` prints a line before each slow step (App Store Connect reads, device families, web build), so a run without a terminal that stalls shows where.
## 0.8.61 · 2026-10-29

View file

@ -4,7 +4,8 @@
// 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 { existsSync, realpathSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import pkg from '../package.json' with { type: 'json' };
import type { Args } from './lib/args';
@ -114,6 +115,8 @@ export async function run(args: Args): Promise<void> {
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(', ')}` : ''}`);
log('doctor', playLine(r.play));
const tcc = protectedInstall(realScript(), homedir());
if (tcc) log('doctor', `WARN this storebound runs from ${tcc}: macOS guards that folder, so a launchd job, CI agent or anything started outside a terminal with access blocks forever when it opens the file (no output, no error). Install it from npm instead: bun add -g storebound@latest (or npm i -g storebound)`);
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 || t.status === 'outdated');
if (!missing.length) { log('doctor', 'everything storebound can use is here'); return; }
@ -125,3 +128,17 @@ export async function run(args: Args): Promise<void> {
}
log('doctor', `${missing.length} missing. Store work needs none of them; screenshots, previews, icons and captures do.`);
}
/** The real path of the running storebound (a bun link / npm link resolves into the checkout). */
function realScript(): string { try { return realpathSync(process.argv[1] ?? ''); } catch { return process.argv[1] ?? ''; } }
/** macOS only: the guarded home folder (Desktop, Documents, Downloads, iCloud Drive) a path lies in, or null. A process
* without access to it (a launchd job, a CI agent) hangs in open() while macOS waits for a consent nobody can give. */
export function protectedInstall(path: string, home: string, platform: string = process.platform): string | null {
if (platform !== 'darwin' || !path || !home) return null;
for (const d of ['Desktop', 'Documents', 'Downloads', 'Library/Mobile Documents']) {
const dir = `${home.endsWith('/') ? home.slice(0, -1) : home}/${d}`; // posix paths: the check is macOS-only
if (path === dir || path.startsWith(dir + '/')) return `~/${d}`;
}
return null;
}

9
tests/doctor-tcc.test.ts Normal file
View file

@ -0,0 +1,9 @@
import { expect, test } from 'bun:test';
import { protectedInstall } from '../cli/doctor';
test('a storebound running from ~/Desktop (a bun link to a checkout) is named; npm / bun global installs are fine', () => {
expect(protectedInstall('/Users/you/Desktop/storebound/dist/storebound.mjs', '/Users/you', 'darwin')).toBe('~/Desktop');
expect(protectedInstall('/Users/you/Documents/x/cli/index.ts', '/Users/you', 'darwin')).toBe('~/Documents');
expect(protectedInstall('/Users/you/.bun/install/global/node_modules/storebound/dist/storebound.mjs', '/Users/you', 'darwin')).toBeNull();
expect(protectedInstall('/Users/you/Desktop/x.mjs', '/Users/you', 'linux')).toBeNull();
});