Harden MVP runtime and fix packaged desktop white screen

This commit is contained in:
Ryan Lancaster
2026-03-18 13:05:14 -04:00
parent 9302a88aea
commit 4a7b3061ed
14 changed files with 359 additions and 29 deletions

View File

@@ -9,7 +9,30 @@ exports.default = async function (context) {
`${context.packager.appInfo.productFilename}.app`,
);
console.log(`Ad-hoc signing: ${appPath}`);
execSync(`xattr -cr "${appPath}"`);
execSync(`codesign --force --deep --sign - "${appPath}"`);
console.log('Ad-hoc signing complete.');
// Remove Finder/iCloud metadata that can make codesign fail with
// "resource fork, Finder information, or similar detritus not allowed".
const cleanupCmds = [
`xattr -cr "${appPath}"`,
`dot_clean -m "${appPath}"`,
`find "${appPath}" -name '._*' -delete`,
`xattr -r -d com.apple.FinderInfo "${appPath}"`,
`xattr -r -d com.apple.ResourceFork "${appPath}"`,
`xattr -r -d com.apple.quarantine "${appPath}"`,
];
for (const cmd of cleanupCmds) {
try {
execSync(cmd, { stdio: 'ignore' });
} catch {
// Cleanup commands are best-effort.
}
}
try {
execSync(`codesign --force --deep --sign - "${appPath}"`, { stdio: 'inherit' });
console.log('Ad-hoc signing complete.');
} catch (err) {
console.warn('Ad-hoc signing skipped due to codesign error. Build artifacts are still usable locally.');
}
};

97
scripts/mvp-check.cjs Normal file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env node
const { spawnSync } = require('child_process');
const args = new Set(process.argv.slice(2));
if (args.has('--help') || args.has('-h')) {
console.log('Project Hub MVP readiness check');
console.log('');
console.log('Usage:');
console.log(' node scripts/mvp-check.cjs [--restart-backend]');
console.log('');
console.log('Environment:');
console.log(' API_BASE default: http://localhost:4000/api');
console.log(' WRITE_API_KEY optional x-api-key for write endpoints');
process.exit(0);
}
const backendPort = process.env.PORT_BACKEND || '4000';
const API_BASE = process.env.API_BASE || `http://localhost:${backendPort}/api`;
const HEALTH_URL = API_BASE.replace(/\/api\/?$/, '') + '/health';
const RESTART_BACKEND = args.has('--restart-backend');
function log(msg) {
console.log(`[mvp-check] ${msg}`);
}
function formatCheck(status, name, detail = '') {
const suffix = detail ? `: ${detail}` : '';
console.log(`[mvp-check] ${status} ${name}${suffix}`);
}
async function checkHealth() {
try {
const response = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(5000) });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return { ok: true };
} catch (err) {
return { ok: false, detail: err.message };
}
}
function runNodeScript(scriptPath, scriptArgs = []) {
const result = spawnSync(process.execPath, [scriptPath, ...scriptArgs], {
env: process.env,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
});
const output = [result.stdout || '', result.stderr || ''].join('').trim();
const lines = output ? output.split(/\r?\n/).filter(Boolean) : [];
const tail = lines.slice(-6).join(' | ');
if (result.error) {
return { ok: false, detail: result.error.message };
}
if (result.status !== 0) {
return { ok: false, detail: tail || `exit ${result.status}` };
}
return { ok: true, detail: tail };
}
async function run() {
log('Starting MVP hardening checks...');
log(`API base: ${API_BASE}`);
const results = [];
const health = await checkHealth();
results.push({ name: 'backend-health', ...health });
formatCheck(health.ok ? 'PASS' : 'FAIL', 'backend-health', health.detail || HEALTH_URL);
const wiring = runNodeScript('scripts/wiring-smoke-test.cjs');
results.push({ name: 'wiring-smoke', ...wiring });
formatCheck(wiring.ok ? 'PASS' : 'FAIL', 'wiring-smoke', wiring.detail);
const persistenceArgs = RESTART_BACKEND ? ['--restart-backend'] : [];
const persistence = runNodeScript('scripts/persistence-self-test.cjs', persistenceArgs);
results.push({ name: RESTART_BACKEND ? 'persistence-restart' : 'persistence', ...persistence });
formatCheck(persistence.ok ? 'PASS' : 'FAIL', RESTART_BACKEND ? 'persistence-restart' : 'persistence', persistence.detail);
const failed = results.filter(r => !r.ok);
if (failed.length > 0) {
log('NOT READY');
log(`Failed checks: ${failed.map(f => f.name).join(', ')}`);
process.exit(1);
}
log('READY');
log('All automated MVP hardening checks passed.');
}
run().catch(err => {
log(`NOT READY: ${err.message}`);
process.exit(1);
});

View File

@@ -15,7 +15,8 @@ if (args.has('--help') || args.has('-h')) {
process.exit(0);
}
const API_BASE = process.env.API_BASE || 'http://localhost:4000/api';
const backendPort = process.env.PORT_BACKEND || '4000';
const API_BASE = process.env.API_BASE || `http://localhost:${backendPort}/api`;
const HEALTH_URL = API_BASE.replace(/\/api\/?$/, '') + '/health';
const WRITE_API_KEY = process.env.WRITE_API_KEY || '';
const SHOULD_RESTART = args.has('--restart-backend');

89
scripts/ports-report.cjs Normal file
View File

@@ -0,0 +1,89 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const net = require('net');
const root = process.cwd();
const envPath = path.join(root, '.env');
function parseEnv(filePath) {
if (!fs.existsSync(filePath)) return {};
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
const env = {};
for (const raw of lines) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const idx = line.indexOf('=');
if (idx <= 0) continue;
const key = line.slice(0, idx).trim();
let value = line.slice(idx + 1).trim();
// Strip trailing comments for unquoted values.
if (!value.startsWith('"') && !value.startsWith("'")) {
const hashIdx = value.indexOf(' #');
if (hashIdx >= 0) value = value.slice(0, hashIdx).trim();
}
env[key] = value;
}
return env;
}
function checkPortOpen(port, host = '127.0.0.1', timeoutMs = 500) {
return new Promise(resolve => {
const socket = new net.Socket();
let settled = false;
const done = (isOpen) => {
if (settled) return;
settled = true;
socket.destroy();
resolve(isOpen);
};
socket.setTimeout(timeoutMs);
socket.once('connect', () => done(true));
socket.once('timeout', () => done(false));
socket.once('error', () => done(false));
socket.connect(Number(port), host);
});
}
async function run() {
const env = parseEnv(envPath);
const backendPort = env.PORT_BACKEND || '4000';
const frontendPort = env.PORT_FRONTEND_DEV || '5173';
const frontendFallback = env.PORT_FRONTEND_DEV_FALLBACK || '5174';
const ports = [
{ name: 'Backend API', key: 'PORT_BACKEND', value: backendPort },
{ name: 'Frontend dev', key: 'PORT_FRONTEND_DEV', value: frontendPort },
{ name: 'Frontend fallback', key: 'PORT_FRONTEND_DEV_FALLBACK', value: frontendFallback },
];
console.log('[ports] Dedicated ports for this project');
console.log(`[ports] Source: ${envPath}`);
console.log('');
for (const p of ports) {
const isOpen = await checkPortOpen(p.value);
const state = isOpen ? 'IN USE' : 'free';
console.log(`[ports] ${p.key}=${p.value} (${p.name}) -> ${state}`);
}
console.log('');
console.log(`[ports] APP_URL=${env.APP_URL || `http://localhost:${frontendPort}`}`);
console.log(`[ports] API_BASE=${env.API_BASE || `http://localhost:${backendPort}/api`}`);
console.log(`[ports] VITE_API_URL=${env.VITE_API_URL || `http://localhost:${backendPort}/api`}`);
if (env.CORS_ORIGIN) console.log(`[ports] CORS_ORIGIN=${env.CORS_ORIGIN}`);
}
run().catch(err => {
console.error(`[ports] Failed: ${err.message}`);
process.exit(1);
});

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env node
const API_BASE = process.env.API_BASE || 'http://localhost:4000/api';
const backendPort = process.env.PORT_BACKEND || '4000';
const API_BASE = process.env.API_BASE || `http://localhost:${backendPort}/api`;
const HEALTH_URL = API_BASE.replace(/\/api\/?$/, '') + '/health';
const WRITE_API_KEY = process.env.WRITE_API_KEY || '';