Harden MVP runtime and fix packaged desktop white screen
This commit is contained in:
@@ -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
97
scripts/mvp-check.cjs
Normal 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);
|
||||
});
|
||||
@@ -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
89
scripts/ports-report.cjs
Normal 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);
|
||||
});
|
||||
@@ -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 || '';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user