-
Notifications
You must be signed in to change notification settings - Fork 52
Feat: Support running scraper as a CLI #724
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,16 +5,25 @@ import { registerHandlers } from './handlers'; | |
| import './security-restrictions'; | ||
| import { restoreOrCreateWindow } from '/@/mainWindow'; | ||
| import { logAppEvent } from './logging/operationLogger'; | ||
| import { scrapeAndUpdateOutputVendors } from './backend'; | ||
| import { getConfig } from './backend/configManager/configManager'; | ||
| import { BudgetTrackingEventEmitter } from './backend/eventEmitters/EventEmitter'; | ||
|
|
||
| // Check for CLI mode | ||
| const isCliScrape = process.argv.includes('--scrape'); | ||
brafdlog marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Prevent electron from running multiple instances. | ||
| * Skip this check in CLI mode to allow running from cron while GUI is open. | ||
| */ | ||
| const isSingleInstance = app.requestSingleInstanceLock(); | ||
| if (!isSingleInstance) { | ||
| app.quit(); | ||
| process.exit(0); | ||
| if (!isCliScrape) { | ||
| const isSingleInstance = app.requestSingleInstanceLock(); | ||
| if (!isSingleInstance) { | ||
| app.quit(); | ||
| process.exit(0); | ||
| } | ||
| app.on('second-instance', restoreOrCreateWindow); | ||
| } | ||
| app.on('second-instance', restoreOrCreateWindow); | ||
|
|
||
| /** | ||
| * Disable Hardware Acceleration to save more system resources. | ||
|
|
@@ -37,17 +46,40 @@ app.on('window-all-closed', () => { | |
| app.on('activate', restoreOrCreateWindow); | ||
|
|
||
| /** | ||
| * Create the application window when the background process is ready. | ||
| * Create the application window when the background process is ready, | ||
| * or run CLI scraping if --scrape flag is passed. | ||
| */ | ||
| app | ||
| .whenReady() | ||
| .then(() => { | ||
| .then(async () => { | ||
| logAppEvent('APP_READY', { | ||
| version: app.getVersion(), | ||
| platform, | ||
| nodeVersion: process.versions.node, | ||
| electronVersion: process.versions.electron, | ||
| cliMode: isCliScrape, | ||
| }); | ||
|
|
||
| if (isCliScrape) { | ||
| // CLI mode: run scraping and exit | ||
brafdlog marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| logAppEvent('CLI_SCRAPE_START'); | ||
| try { | ||
| const config = await getConfig(); | ||
| const eventPublisher = new BudgetTrackingEventEmitter(); | ||
| eventPublisher.onAny((eventName, eventData) => { | ||
| console.log(`[${eventName}]`, eventData?.message ?? ''); | ||
| }); | ||
| await scrapeAndUpdateOutputVendors(config, eventPublisher); | ||
| logAppEvent('CLI_SCRAPE_SUCCESS'); | ||
| app.quit(); | ||
| } catch (error) { | ||
| logAppEvent('CLI_SCRAPE_FAILED', { errorMessage: (error as Error).message }); | ||
brafdlog marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+67
to
+74
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm from mobile, but what is the difference between console.log and logAppEvent? If the logAppEvent is for special lifecycle keys, why are those keys not enum?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logAppEvent writes structured logs to electron-log (persistent log files), while console.log here outputs scraping progress to stdout for CLI users running from terminal/cron. They serve different purposes — logAppEvent for persistent diagnostics, console for real-time CLI feedback. |
||
| console.error('CLI scrape failed:', error); | ||
| app.exit(1); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| return restoreOrCreateWindow(); | ||
| }) | ||
| .catch((e) => { | ||
|
|
@@ -79,14 +111,14 @@ app | |
|
|
||
| /** | ||
| * Check for app updates, install it in background and notify user that new version was installed. | ||
| * No reason run this in non-production build. | ||
| * No reason run this in non-production build or CLI mode. | ||
| * @see https://www.electron.build/auto-update.html#quick-setup-guide | ||
| * | ||
| * Note: It may throw "ENOENT: no such file app-update.yml" | ||
| * if you compile production app without publishing it to distribution server. | ||
| * Like `yarn compile` does. It's ok 😅 | ||
| */ | ||
| if (import.meta.env.PROD) { | ||
| if (import.meta.env.PROD && !isCliScrape) { | ||
| app | ||
| .whenReady() | ||
| .then(async () => { | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this file? Is it for those who want to really cron it from source, and you trying to avoid recompiling every time, but only when needed?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { execSync } from 'child_process'; | ||
| import { statSync, existsSync, readdirSync } from 'fs'; | ||
| import { resolve } from 'path'; | ||
|
|
||
| const projectRoot = resolve(import.meta.dirname, '..'); | ||
|
|
||
| function findSourceFiles(dir) { | ||
| const files = []; | ||
| const entries = readdirSync(dir, { withFileTypes: true }); | ||
|
|
||
| for (const entry of entries) { | ||
| const fullPath = resolve(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| files.push(...findSourceFiles(fullPath)); | ||
| } else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) { | ||
| files.push(fullPath); | ||
| } | ||
| } | ||
|
|
||
| return files; | ||
| } | ||
|
|
||
| function getLatestSourceModificationTime(sourceDir) { | ||
| try { | ||
| const sourceFiles = findSourceFiles(resolve(projectRoot, sourceDir)); | ||
|
|
||
| let latestTime = 0; | ||
| for (const file of sourceFiles) { | ||
| const stat = statSync(file); | ||
| if (stat.mtimeMs > latestTime) latestTime = stat.mtimeMs; | ||
| } | ||
| return latestTime; | ||
| } catch { | ||
| return Date.now(); // If error, assume rebuild needed | ||
| } | ||
| } | ||
|
|
||
| function getBuildOutputModificationTime(distFile) { | ||
| try { | ||
| if (!existsSync(distFile)) return 0; | ||
| return statSync(distFile).mtimeMs; | ||
| } catch { | ||
| return 0; | ||
| } | ||
| } | ||
|
|
||
| const mainSourceLastModified = getLatestSourceModificationTime('packages/main/src'); | ||
| const preloadSourceLastModified = getLatestSourceModificationTime('packages/preload/src'); | ||
| const mainBuildLastModified = getBuildOutputModificationTime(resolve(projectRoot, 'packages/main/dist/index.js')); | ||
| const preloadBuildLastModified = getBuildOutputModificationTime(resolve(projectRoot, 'packages/preload/dist/index.js')); | ||
|
|
||
| const mainNeedsRebuild = mainSourceLastModified > mainBuildLastModified; | ||
| const preloadNeedsRebuild = preloadSourceLastModified > preloadBuildLastModified; | ||
|
|
||
| if (mainNeedsRebuild || preloadNeedsRebuild) { | ||
| console.log('Source files changed, rebuilding...'); | ||
| if (mainNeedsRebuild) { | ||
| console.log('Building main...'); | ||
| execSync('yarn build:main', { stdio: 'inherit', cwd: projectRoot }); | ||
| } | ||
| if (preloadNeedsRebuild) { | ||
| console.log('Building preload...'); | ||
| execSync('yarn build:preload', { stdio: 'inherit', cwd: projectRoot }); | ||
| } | ||
| } else { | ||
| console.log('Build is up to date, skipping...'); | ||
| } | ||
|
|
||
| console.log('Starting scrape...'); | ||
| execSync('electron . --scrape', { stdio: 'inherit', cwd: projectRoot }); |
Uh oh!
There was an error while loading. Please reload this page.