/** * Keep clicker/requirements.txt applied, cheaply. * * Called from start-all.mjs on every boot, and from update-check.mjs when a * pull touches requirements.txt. Running `pip install` unconditionally would * add seconds to every start and fail outright on a machine whose network is * not up yet, so the work is guarded by a hash plus an import probe. * * Failure is never fatal: the dashboard and the trading loops do not need * Python. Only the clicker does. */ import { createHash } from 'node:crypto'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const REQUIREMENTS = path.join(ROOT, 'clicker', 'requirements.txt'); const HASH_FILE = path.join(ROOT, 'scripts', '.deps-hash'); const PYTHON_CMD_FILE = path.join(ROOT, 'scripts', '.python-cmd'); /** Run ` `; cmd may carry arguments of its own, e.g. "py -3". */ function run(cmd, args, opts = {}) { const parts = cmd.split(/\s+/); return spawnSync(parts[0], [...parts.slice(1), ...args], { cwd: ROOT, encoding: 'utf8', shell: process.platform === 'win32', ...opts, }); } /** A real interpreter answers with its major version. The Microsoft Store stub * that Windows puts on PATH produces nothing, which is how we tell them apart. */ function isRealPython(cmd) { const r = run(cmd, ['-c', 'import sys;print(sys.version_info[0])']); return r.status === 0 && (r.stdout ?? '').trim() === '3'; } export function resolvePython() { // setup-windows.bat already did this detection properly, stub check included, // and recorded what it found. Prefer that over guessing again. if (existsSync(PYTHON_CMD_FILE)) { const saved = readFileSync(PYTHON_CMD_FILE, 'utf8').trim(); if (saved && isRealPython(saved)) return saved; } for (const candidate of ['py -3', 'python3', 'python']) { if (isRealPython(candidate)) return candidate; } return null; } export function ensurePythonDeps({ log = console.log, force = false } = {}) { if (!existsSync(REQUIREMENTS)) { return { ok: true, action: 'skipped', reason: 'no requirements.txt' }; } const python = resolvePython(); if (!python) { log('[deps] python not found — clicker dependencies skipped (dashboard is unaffected)'); return { ok: false, action: 'skipped', reason: 'no python' }; } const wanted = createHash('sha256').update(readFileSync(REQUIREMENTS)).digest('hex'); const recorded = existsSync(HASH_FILE) ? readFileSync(HASH_FILE, 'utf8').trim() : ''; const importsOk = run(python, ['-c', 'import pyautogui']).status === 0; if (!force && wanted === recorded && importsOk) { return { ok: true, action: 'up-to-date', python }; } log(`[deps] installing clicker dependencies via "${python}"${importsOk ? '' : ' (pyautogui does not import)'}`); const install = run(python, [ '-m', 'pip', 'install', '--disable-pip-version-check', '--quiet', '-r', path.join('clicker', 'requirements.txt'), ], { stdio: 'inherit' }); if (install.status !== 0) { log('[deps] pip install failed — the clicker will not work until this is fixed'); return { ok: false, action: 'failed', python }; } if (run(python, ['-c', 'import pyautogui']).status !== 0) { log('[deps] pip reported success but pyautogui still does not import'); return { ok: false, action: 'failed', python }; } writeFileSync(HASH_FILE, wanted + '\n'); log('[deps] clicker dependencies ready'); return { ok: true, action: 'installed', python }; } // Allow `node scripts/ensure-python-deps.mjs` directly. if (import.meta.url === `file://${process.argv[1]}`) { const result = ensurePythonDeps({ force: process.argv.includes('--force') }); console.log(JSON.stringify(result)); process.exit(result.ok ? 0 : 1); }