#!/usr/bin/env node /** * Cross-platform source smoke check for the desktop distribution contract. * It deliberately does not require Windows, a native shell, or a browser. * Run from the repository root: node apps/desktop/scripts/smoke-desktop.mjs */ import { readFile, stat } from 'node:fs/promises'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const repoRoot = resolve(desktopRoot, '../..'); const manifestPath = resolve(desktopRoot, 'desktop-manifest.json'); const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); const results = []; function check(id, description, pass, details = '') { results.push({ id, description, pass: Boolean(pass), ...(details ? { details } : {}) }); } function unique(values) { return [...new Set(values)]; } function sorted(values) { return [...values].sort(); } function relativeToRepo(path) { return resolve(desktopRoot, path).replace(`${repoRoot}/`, ''); } check('manifest.schema', 'desktop manifest has the supported source-contract shape', manifest.schema === 1 && manifest.shell === 'web-ui' && manifest.status === 'source-contract' && manifest.entrypoint === '../web/index.html' && Array.isArray(manifest.assets) && Array.isArray(manifest.runtime_config_keys) && typeof manifest.routes_source === 'string'); const assetPaths = manifest.assets.map(relativeToRepo); const missingAssets = []; for (const path of assetPaths) { try { if (!(await stat(resolve(repoRoot, path))).isFile()) missingAssets.push(path); } catch { missingAssets.push(path); } } check('assets.present', 'all desktop-referenced web assets exist', missingAssets.length === 0, missingAssets.join(', ')); check('assets.no-duplicates', 'desktop contract reuses web assets instead of maintaining a second UI copy', manifest.assets.every(asset => asset.startsWith('../web/'))); const webConfig = await readFile(resolve(repoRoot, 'apps/web/config.js'), 'utf8'); check('config.runtime-keys', 'desktop supports only the non-secret web runtime configuration keys', manifest.runtime_config_keys.length === 2 && manifest.runtime_config_keys.includes('apiBase') && manifest.runtime_config_keys.includes('assetVersion') && !/(token|password|secret|private.?key)\s*[:=]/i.test(webConfig)); const html = await readFile(resolve(repoRoot, 'apps/web/index.html'), 'utf8'); const linkedAssets = unique([...html.matchAll(/(?:src|href)=["']([^"'#?]+)(?:\?[^"']*)?["']/gi)] .map(match => match[1]).filter(asset => !asset.startsWith('http') && !asset.startsWith('data:')) .map(asset => asset.replace(/^\.\//, ''))); const manifestWebNames = new Set(manifest.assets.map(asset => asset.replace('../web/', ''))); const missingLinkedAssets = linkedAssets.filter(asset => !manifestWebNames.has(asset)); check('assets.html-parity', 'desktop manifest covers every local asset linked by web index.html', missingLinkedAssets.length === 0, missingLinkedAssets.join(', ')); const routeSource = await readFile(resolve(desktopRoot, manifest.routes_source), 'utf8'); const routesBlock = routeSource.match(/const routes = \[(.*?)];/s)?.[1] || ''; const webRoutes = unique([...routesBlock.matchAll(/['"](\/api\/v1\/[^'"]+)['"]/g)].map(match => match[1])); const desktopRoutes = Array.isArray(manifest.routes) ? unique(manifest.routes) : webRoutes; check('routes.source', 'route source contains the web API contract', webRoutes.length > 0); check('routes.parity', 'desktop route contract is exactly the web route contract', JSON.stringify(sorted(desktopRoutes)) === JSON.stringify(sorted(webRoutes)), `web=${webRoutes.length}, desktop=${desktopRoutes.length}`); const failed = results.filter(result => !result.pass); const report = { schema: 1, harness: 'prospectos-desktop-source-smoke', chromium_required: false, windows_required: false, pass: failed.length === 0, totals: { checks: results.length, passed: results.length - failed.length, failed: failed.length }, checks: results }; console.log(JSON.stringify(report, null, 2)); if (failed.length) process.exitCode = 1;