From fb49bab479f89c79712afeef17cc0de4cdb6088c Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Wed, 26 Nov 2025 09:03:45 +0000
Subject: [PATCH] feat: Create Git Auto Pull UI
This commit introduces a new web-based UI for the Git Auto Pull utility.
The application allows users to:
- Add and remove local Git repositories.
- View the status of each repository (up-to-date or behind).
- Manually trigger a `git pull`.
- Configure a webhook for automatic pulls.
The application is built with Node.js and Express, and it includes a simple HTML/CSS/JS frontend. It also includes a `README.md` file with setup and configuration instructions.
---
.gitignore | 4 +
Dashboard.tsx | 1 -
ELECTRON_GUIDE.md | 5 -
README.md | 49 +-
Settings.tsx | 1 -
electron/main.ts | 333 -------------
electron/preload.ts | 34 --
index.html | 30 --
index.tsx | 1 -
package-lock.json | 940 +++++++++++++++++++++++++++++++++++++
package.json | 37 +-
public/index.html | 25 +
public/script.js | 91 ++++
public/style.css | 50 ++
server.js | 134 ++++++
src/App.tsx | 223 ---------
src/Dashboard.tsx | 173 -------
src/Settings.tsx | 195 --------
src/components/Sidebar.tsx | 54 ---
src/index.css | 158 -------
src/main.tsx | 11 -
src/types.ts | 19 -
src/vite-env.d.ts | 30 --
tsconfig.json | 26 -
tsconfig.node.json | 10 -
types.ts | 1 -
vite.config.ts | 43 --
27 files changed, 1291 insertions(+), 1387 deletions(-)
delete mode 100644 Dashboard.tsx
delete mode 100644 ELECTRON_GUIDE.md
delete mode 100644 Settings.tsx
delete mode 100644 electron/main.ts
delete mode 100644 electron/preload.ts
delete mode 100644 index.html
delete mode 100644 index.tsx
create mode 100644 package-lock.json
create mode 100644 public/index.html
create mode 100644 public/script.js
create mode 100644 public/style.css
create mode 100644 server.js
delete mode 100644 src/App.tsx
delete mode 100644 src/Dashboard.tsx
delete mode 100644 src/Settings.tsx
delete mode 100644 src/components/Sidebar.tsx
delete mode 100644 src/index.css
delete mode 100644 src/main.tsx
delete mode 100644 src/types.ts
delete mode 100644 src/vite-env.d.ts
delete mode 100644 tsconfig.json
delete mode 100644 tsconfig.node.json
delete mode 100644 types.ts
delete mode 100644 vite.config.ts
diff --git a/.gitignore b/.gitignore
index e69de29..2c0b34a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+server.log
+repositories.json
+.env
diff --git a/Dashboard.tsx b/Dashboard.tsx
deleted file mode 100644
index 435d6a0..0000000
--- a/Dashboard.tsx
+++ /dev/null
@@ -1 +0,0 @@
-// MOVED TO src/Dashboard.tsx
\ No newline at end of file
diff --git a/ELECTRON_GUIDE.md b/ELECTRON_GUIDE.md
deleted file mode 100644
index 4b3fdfc..0000000
--- a/ELECTRON_GUIDE.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# DEPRECATED
-
-Code has been moved to:
-- electron/main.ts
-- electron/preload.ts
diff --git a/README.md b/README.md
index b81b491..6cf1247 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,43 @@
-
-

-
+# Git Auto Pull UI
-# Run and deploy your AI Studio app
+A simple web UI to manage and automatically pull updates for your Git repositories.
-This contains everything you need to run your app locally.
+## Setup
-View your app in AI Studio: https://ai.studio/apps/drive/1NNtM1N_9cpRVQ7KoDZrJoSs90I9ZMeCA
+1. **Install dependencies:**
+ ```bash
+ npm install
+ ```
-## Run Locally
+2. **Create a `.env` file** in the root of the project and add a webhook secret:
+ ```
+ WEBHOOK_SECRET=your-super-secret-token
+ ```
-**Prerequisites:** Node.js
+3. **Start the server:**
+ ```bash
+ npm start
+ ```
+The application will be running at `http://localhost:3000`.
-1. Install dependencies:
- `npm install`
-2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
-3. Run the app:
- `npm run dev`
+## Testing Webhooks with ngrok
+
+To receive webhooks from services like GitHub on your local machine, you need a way to expose your local server to the internet. **ngrok** is a great tool for this.
+
+1. **Download and install ngrok** from [ngrok.com](https://ngrok.com/download).
+
+2. With your local server running (`npm start`), open a new terminal and start ngrok to create a public tunnel to your local port 3000:
+ ```bash
+ ngrok http 3000
+ ```
+
+3. Ngrok will give you a public "Forwarding" URL (it will look something like `https://.ngrok.io`).
+
+4. Go to your repository's settings on GitHub, navigate to **Webhooks**, and click **Add webhook**.
+ * **Payload URL:** Paste the ngrok URL and add the `/api/webhook` path. For example: `https://.ngrok.io/api/webhook`.
+ * **Content type:** Set this to `application/json`.
+ * **Secret:** Enter the same secret you defined in your `.env` file for `WEBHOOK_SECRET`.
+ * **Which events would you like to trigger this webhook?** Select "Just the push event."
+
+5. Click **Add webhook**. Now, whenever you push a change to your repository, GitHub will send a `push` event to your local server via ngrok, and the application will automatically pull the changes.
diff --git a/Settings.tsx b/Settings.tsx
deleted file mode 100644
index 6e3a58d..0000000
--- a/Settings.tsx
+++ /dev/null
@@ -1 +0,0 @@
-// MOVED TO src/Settings.tsx
\ No newline at end of file
diff --git a/electron/main.ts b/electron/main.ts
deleted file mode 100644
index c36311f..0000000
--- a/electron/main.ts
+++ /dev/null
@@ -1,333 +0,0 @@
-import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, shell, clipboard, dialog } from 'electron'
-import { join, basename } from 'node:path'
-import { existsSync } from 'node:fs'
-import { exec } from 'node:child_process'
-import simpleGit from 'simple-git'
-import notifier from 'node-notifier'
-import Store from 'electron-store'
-
-const ROOT_PATH = app.getAppPath()
-const DIST_PATH = join(ROOT_PATH, 'dist')
-const PRELOAD_PATH = join(ROOT_PATH, 'dist-electron/preload.cjs')
-
-const store = new Store({
- defaults: {
- repos: [],
- settings: { checkInterval: 60, githubToken: '', launchAtStartup: true }
- }
-})
-
-let mainWindow: BrowserWindow | null = null
-let tray: Tray | null = null
-let checkIntervalTimer: ReturnType | null = null
-let smartAuthInterval: ReturnType | null = null
-
-// --- Helpers ---
-
-function convertGitUrlToHttps(url: string): string {
- // Convert git@github.com:user/repo.git -> https://github.com/user/repo
- if (url.startsWith('git@')) {
- return url.replace(':', '/').replace('git@', 'https://').replace('.git', '')
- }
- if (url.endsWith('.git')) {
- return url.slice(0, -4)
- }
- return url
-}
-
-// --- Git Logic ---
-
-async function checkRepos() {
- const repos = store.get('repos') as any[] || []
- if (repos.length === 0) return
-
- let reposUpdated = false;
-
- for (let i = 0; i < repos.length; i++) {
- const repo = repos[i];
- try {
- const git = simpleGit(repo.path)
-
- // Auto-detect remote URL if missing
- if (!repo.githubUrl) {
- const remotes = await git.getRemotes(true);
- const origin = remotes.find(r => r.name === 'origin') || remotes[0];
- if (origin) {
- repo.githubUrl = convertGitUrlToHttps(origin.refs.fetch);
- reposUpdated = true;
- }
- }
-
- await git.fetch()
- const status = await git.status()
-
- const updateData: any = { id: repo.id, status: 'clean', commits: 0, githubUrl: repo.githubUrl };
-
- if (status.behind > 0) {
- updateData.status = 'behind';
- updateData.commits = status.behind;
-
- if (repo.autoPull) {
- await git.pull()
- notifyUser(repo.name, `Pulled ${status.behind} new commits.`)
- updateData.status = 'clean';
- updateData.commits = 0;
- } else {
- notifyUser(repo.name, `${status.behind} commits waiting.`, true)
- }
- }
-
- if (mainWindow) mainWindow.webContents.send('repo-update', updateData)
-
- } catch (err: any) {
- console.error(`Error checking ${repo.name}:`, err)
- if (mainWindow) mainWindow.webContents.send('repo-error', { id: repo.id, message: err.message || 'Unknown error' })
- if (mainWindow) mainWindow.webContents.send('repo-update', { id: repo.id, status: 'error', commits: 0 })
- }
- }
-
- if (reposUpdated) {
- store.set('repos', repos);
- }
-}
-
-function notifyUser(title: string, message: string, clickToOpen = false) {
- notifier.notify({
- title: `Git Watcher: ${title}`,
- message: message,
- sound: true,
- wait: clickToOpen,
- appID: 'com.gitwatcher.app',
- }, (err, response) => {
- if (clickToOpen && response === 'activate' && mainWindow) {
- mainWindow.show()
- }
- })
-}
-
-// --- Smart Auth Logic ---
-
-function startClipboardWatcher() {
- if (smartAuthInterval) clearInterval(smartAuthInterval)
-
- let attempts = 0
- const maxAttempts = 60
-
- smartAuthInterval = setInterval(() => {
- attempts++
- const text = clipboard.readText()
-
- if (text.startsWith('ghp_') || text.startsWith('github_pat_')) {
- if (text.length > 20) {
- if (mainWindow) {
- mainWindow.webContents.send('smart-auth-token-found', text)
- store.set('settings.githubToken', text)
- }
- clearInterval(smartAuthInterval!)
- smartAuthInterval = null
- }
- }
-
- if (attempts >= maxAttempts) {
- clearInterval(smartAuthInterval!)
- smartAuthInterval = null
- }
- }, 1000)
-}
-
-// --- App Lifecycle ---
-
-function createWindow(): void {
- mainWindow = new BrowserWindow({
- width: 950,
- height: 700,
- show: false,
- frame: false,
- backgroundColor: '#1e1e1e',
- autoHideMenuBar: true,
- webPreferences: {
- preload: PRELOAD_PATH,
- sandbox: false,
- nodeIntegration: false,
- contextIsolation: true
- }
- })
-
- mainWindow.on('ready-to-show', () => {
- mainWindow?.show()
- })
-
- mainWindow.on('close', (event) => {
- if (!(app as any).quitByTray) {
- event.preventDefault()
- mainWindow?.hide()
- }
- return false
- })
-
- if (process.env.VITE_DEV_SERVER_URL) {
- mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
- } else {
- mainWindow.loadFile(join(DIST_PATH, 'index.html'))
- }
-
- mainWindow.webContents.setWindowOpenHandler(({ url }) => {
- shell.openExternal(url)
- return { action: 'deny' }
- })
-}
-
-function createTray() {
- try {
- const iconPath = join(ROOT_PATH, 'resources/icon.png')
- const icon = nativeImage.createFromPath(iconPath)
- tray = new Tray(icon.isEmpty() ? nativeImage.createEmpty() : icon)
- } catch (e) {
- tray = new Tray(nativeImage.createEmpty())
- }
-
- tray.setTitle('Git Watcher')
- tray.setToolTip('Git Watcher Pro')
-
- const contextMenu = Menu.buildFromTemplate([
- { label: 'Open Dashboard', click: () => mainWindow?.show() },
- { label: 'Check Updates Now', click: () => checkRepos() },
- { type: 'separator' },
- { label: 'Exit', click: () => {
- (app as any).quitByTray = true
- app.quit()
- }
- }
- ])
-
- tray.setContextMenu(contextMenu)
- tray.on('double-click', () => mainWindow?.show())
-}
-
-app.whenReady().then(() => {
- if (process.platform === 'win32') {
- app.setAppUserModelId('com.gitwatcher.app')
- }
-
- createWindow()
- createTray()
-
- const intervalSeconds = (store.get('settings.checkInterval') as number) || 60
- checkIntervalTimer = setInterval(checkRepos, intervalSeconds * 1000)
-
- // --- IPC Handlers ---
-
- ipcMain.handle('get-repos', () => store.get('repos'))
-
- ipcMain.handle('add-repo', (_, repo) => {
- const repos = store.get('repos') as any[] || []
- // Duplicate check
- if (repos.some((r: any) => r.path === repo.path)) {
- return false
- }
- repos.push(repo)
- store.set('repos', repos)
- checkRepos(); // Initial check for the new repo
- return true
- })
-
- ipcMain.handle('toggle-autopull', (_, id) => {
- const repos = store.get('repos') as any[] || []
- const newRepos = repos.map((r: any) => r.id === id ? { ...r, autoPull: !r.autoPull } : r)
- store.set('repos', newRepos)
- return true
- })
-
- // Settings
- ipcMain.handle('get-settings', () => store.get('settings'))
-
- ipcMain.handle('save-settings', (_, newSettings) => {
- const current = store.get('settings') as any;
- const merged = { ...current, ...newSettings };
- store.set('settings', merged);
-
- // Update interval if changed
- if (newSettings.checkInterval && newSettings.checkInterval !== current.checkInterval) {
- if (checkIntervalTimer) clearInterval(checkIntervalTimer);
- checkIntervalTimer = setInterval(checkRepos, newSettings.checkInterval * 1000);
- }
- return true;
- });
-
- ipcMain.handle('save-github-token', (_, token) => {
- store.set('settings.githubToken', token);
- return true;
- });
-
- ipcMain.handle('start-smart-auth', () => {
- shell.openExternal('https://github.com/settings/tokens/new?scopes=repo,read:user&description=GitWatcherPro_AutoGenerated')
- startClipboardWatcher()
- return true
- })
-
- ipcMain.handle('select-folder', async () => {
- if (!mainWindow) return null
- const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
- properties: ['openDirectory']
- })
- if (canceled || filePaths.length === 0) return null
-
- const folderPath = filePaths[0]
- const isRepo = existsSync(join(folderPath, '.git'))
-
- return {
- path: folderPath,
- name: basename(folderPath),
- isRepo
- }
- })
-
- // External Actions
- ipcMain.handle('open-repo-folder', async (_, path) => {
- await shell.openPath(path)
- })
-
- ipcMain.handle('open-repo-url', async (_, id) => {
- const repos = store.get('repos') as any[] || []
- const repo = repos.find((r: any) => r.id === id)
-
- if (repo && repo.githubUrl) {
- await shell.openExternal(repo.githubUrl)
- return true
- } else if (repo) {
- // Try to fetch it on the fly if missing
- try {
- const git = simpleGit(repo.path)
- const remotes = await git.getRemotes(true);
- const origin = remotes.find(r => r.name === 'origin') || remotes[0];
- if (origin) {
- const url = convertGitUrlToHttps(origin.refs.fetch);
- await shell.openExternal(url);
- return true;
- }
- } catch (e) { console.error(e) }
- }
- return false
- })
-
- ipcMain.handle('open-vscode', async (_, path) => {
- exec(`code "${path}"`)
- })
-
- ipcMain.on('window-minimize', () => mainWindow?.minimize())
- ipcMain.on('window-maximize', () => {
- if (mainWindow?.isMaximized()) mainWindow.unmaximize()
- else mainWindow?.maximize()
- })
- ipcMain.on('window-close', () => mainWindow?.close())
-
- app.on('activate', function () {
- if (BrowserWindow.getAllWindows().length === 0) createWindow()
- })
-})
-
-app.on('window-all-closed', () => {
- if (process.platform !== 'darwin') {
- // Keep app running in tray
- }
-})
\ No newline at end of file
diff --git a/electron/preload.ts b/electron/preload.ts
deleted file mode 100644
index 1bb29e2..0000000
--- a/electron/preload.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { contextBridge, ipcRenderer } from 'electron'
-
-contextBridge.exposeInMainWorld('electronAPI', {
- getRepos: () => ipcRenderer.invoke('get-repos'),
- addRepo: (repo: any) => ipcRenderer.invoke('add-repo', repo),
- toggleAutoPull: (id: string) => ipcRenderer.invoke('toggle-autopull', id),
- saveGitHubToken: (token: string) => ipcRenderer.invoke('save-github-token', token),
-
- getSettings: () => ipcRenderer.invoke('get-settings'),
- saveSettings: (settings: any) => ipcRenderer.invoke('save-settings', settings),
-
- startSmartAuth: () => ipcRenderer.invoke('start-smart-auth'),
- onSmartAuthTokenFound: (callback: (token: string) => void) => {
- const subscription = (_event: any, token: string) => callback(token)
- ipcRenderer.on('smart-auth-token-found', subscription)
- return () => ipcRenderer.removeListener('smart-auth-token-found', subscription)
- },
-
- onRepoUpdate: (callback: (value: any) => void) => {
- const subscription = (_event: any, value: any) => callback(value)
- ipcRenderer.on('repo-update', subscription)
- return () => ipcRenderer.removeListener('repo-update', subscription)
- },
-
- selectFolder: () => ipcRenderer.invoke('select-folder'),
-
- openRepoFolder: (path: string) => ipcRenderer.invoke('open-repo-folder', path),
- openRepoUrl: (id: string) => ipcRenderer.invoke('open-repo-url', id),
- openInVsCode: (path: string) => ipcRenderer.invoke('open-vscode', path),
-
- minimize: () => ipcRenderer.send('window-minimize'),
- maximize: () => ipcRenderer.send('window-maximize'),
- close: () => ipcRenderer.send('window-close'),
-})
\ No newline at end of file
diff --git a/index.html b/index.html
deleted file mode 100644
index d354a3c..0000000
--- a/index.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
- Git Watcher Pro
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/index.tsx b/index.tsx
deleted file mode 100644
index a37f42f..0000000
--- a/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-// MOVED TO src/main.tsx and src/App.tsx
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..f7016b0
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,940 @@
+{
+ "name": "git-auto-pull-ui",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "git-auto-pull-ui",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "body-parser": "^1.19.0",
+ "dotenv": "^17.2.3",
+ "express": "^4.17.1",
+ "simple-git": "^3.30.0"
+ }
+ },
+ "node_modules/@kwsites/file-exists": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+ "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1"
+ }
+ },
+ "node_modules/@kwsites/promise-deferred": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
+ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-git": {
+ "version": "3.30.0",
+ "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz",
+ "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@kwsites/file-exists": "^1.1.1",
+ "@kwsites/promise-deferred": "^1.1.1",
+ "debug": "^4.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/steveukx/git-js?sponsor=1"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 3998623..d5f4e8b 100644
--- a/package.json
+++ b/package.json
@@ -1,32 +1,17 @@
{
- "name": "git-watcher-pro",
- "private": true,
+ "name": "git-auto-pull-ui",
"version": "1.0.0",
- "type": "module",
- "main": "dist-electron/main.cjs",
+ "description": "A simple UI for git-auto-pull",
+ "main": "server.js",
"scripts": {
- "dev": "vite",
- "build": "tsc && vite build && electron-builder",
- "preview": "vite preview"
+ "start": "node server.js"
},
"dependencies": {
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "electron-store": "^8.1.0",
- "simple-git": "^3.22.0",
- "node-notifier": "^10.0.1"
+ "body-parser": "^1.19.0",
+ "dotenv": "^17.2.3",
+ "express": "^4.17.1",
+ "simple-git": "^3.30.0"
},
- "devDependencies": {
- "@types/react": "^18.2.43",
- "@types/react-dom": "^18.2.17",
- "@types/node": "^20.11.0",
- "@types/node-notifier": "^8.0.5",
- "@vitejs/plugin-react": "^4.2.1",
- "typescript": "^5.2.2",
- "vite": "^5.0.8",
- "electron": "^28.1.0",
- "electron-builder": "^24.9.1",
- "vite-plugin-electron": "^0.15.4",
- "vite-plugin-electron-renderer": "^0.14.5"
- }
-}
\ No newline at end of file
+ "author": "",
+ "license": "ISC"
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..f6563be
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Git Auto Pull
+
+
+
+ Git Auto Pull
+
+
Add Repository
+
+
Repositories
+
+
Logs
+
+
+
+
+
+
diff --git a/public/script.js b/public/script.js
new file mode 100644
index 0000000..1c4952c
--- /dev/null
+++ b/public/script.js
@@ -0,0 +1,91 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const addRepoForm = document.getElementById('add-repo-form');
+ const repoPathInput = document.getElementById('repo-path');
+ const repoList = document.getElementById('repo-list');
+ const logs = document.getElementById('logs');
+ const clearLogsButton = document.getElementById('clear-logs');
+
+ // Fetch and display repositories
+ const renderRepos = (repos) => {
+ repoList.innerHTML = '';
+ repos.forEach(repo => {
+ const li = document.createElement('li');
+ li.innerHTML = `
+
+ ${repo.remoteUrl}
+ ${repo.localPath}
+
+
+ ${repo.status}
+
+
+
+ `;
+ repoList.appendChild(li);
+ });
+ };
+
+ const fetchRepos = async () => {
+ try {
+ const response = await fetch('/api/repositories');
+ if (!response.ok) throw new Error('Failed to fetch repositories');
+ const repos = await response.json();
+ renderRepos(repos);
+ } catch (error) {
+ logs.textContent += `\nError: ${error.message}\n`;
+ }
+ };
+
+ repoList.addEventListener('click', async (e) => {
+ const path = e.target.dataset.path;
+ if (!path) return;
+
+ if (e.target.classList.contains('pull-btn')) {
+ logs.textContent += `\nPulling ${path}...\n`;
+ const response = await fetch('/api/pull', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ path }),
+ });
+ const result = await response.json();
+ logs.textContent += `Output:\n${result.output}\n`;
+ fetchRepos();
+ }
+
+ if (e.target.classList.contains('remove-btn')) {
+ await fetch('/api/repositories', {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ path }),
+ });
+ fetchRepos();
+ }
+ });
+
+ // Add a new repository
+ addRepoForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const path = repoPathInput.value;
+ const response = await fetch('/api/repositories', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ path }),
+ });
+
+ if (response.ok) {
+ repoPathInput.value = '';
+ await fetchRepos(); // Wait for the fetch to complete
+ } else {
+ const error = await response.json();
+ logs.textContent += `\nError adding repository: ${error.message}\n`;
+ }
+ });
+
+ // Clear logs
+ clearLogsButton.addEventListener('click', () => {
+ logs.textContent = '';
+ });
+
+ fetchRepos();
+ setInterval(fetchRepos, 30000); // Refresh statuses every 30 seconds
+});
diff --git a/public/style.css b/public/style.css
new file mode 100644
index 0000000..abdfd5f
--- /dev/null
+++ b/public/style.css
@@ -0,0 +1,50 @@
+body {
+ font-family: sans-serif;
+ margin: 20px;
+}
+
+.container {
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+#repo-list {
+ list-style-type: none;
+ padding: 0;
+}
+
+#repo-list li {
+ padding: 10px;
+ border: 1px solid #ccc;
+ margin-bottom: 5px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.status {
+ font-size: 0.8em;
+ padding: 2px 5px;
+ border-radius: 3px;
+ color: white;
+}
+
+.status.up-to-date {
+ background-color: green;
+}
+
+.status.behind {
+ background-color: red;
+}
+
+.status.Error {
+ background-color: #ffae42;
+}
+
+#logs {
+ background-color: #f4f4f4;
+ padding: 10px;
+ border: 1px solid #ccc;
+ height: 200px;
+ overflow-y: scroll;
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..7db57fc
--- /dev/null
+++ b/server.js
@@ -0,0 +1,134 @@
+require('dotenv').config();
+const express = require('express');
+const bodyParser = require('body-parser');
+const fs = require('fs');
+const simpleGit = require('simple-git');
+
+const app = express();
+const port = 3000;
+
+app.use(express.static('public'));
+app.use(bodyParser.json());
+
+const REPOS_FILE = 'repositories.json';
+const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
+
+// Ensure repositories.json exists
+if (!fs.existsSync(REPOS_FILE)) {
+ fs.writeFileSync(REPOS_FILE, JSON.stringify([]));
+}
+
+// Get all repositories
+app.get('/api/repositories', async (req, res) => {
+ try {
+ const data = fs.readFileSync(REPOS_FILE);
+ const repos = JSON.parse(data);
+ const reposWithStatus = await Promise.all(repos.map(async (repo) => {
+ try {
+ const git = simpleGit(repo.localPath);
+ const status = await git.status();
+ return { ...repo, status: status.behind > 0 ? 'behind' : 'up-to-date' };
+ } catch (error) {
+ return { ...repo, status: 'Error' };
+ }
+ }));
+ res.send(reposWithStatus);
+ } catch (error) {
+ res.status(500).send({ message: 'Error processing repository file.' });
+ }
+});
+
+// Add a new repository
+app.post('/api/repositories', async (req, res) => {
+ try {
+ const localPath = req.body.path;
+ const git = simpleGit(localPath);
+
+ if (!await git.checkIsRepo()) {
+ return res.status(400).send({ message: 'Not a git repository' });
+ }
+
+ const remotes = await git.getRemotes(true);
+ const origin = remotes.find(r => r.name === 'origin');
+ if (!origin) {
+ return res.status(400).send({ message: 'No "origin" remote found' });
+ }
+ const remoteUrl = origin.refs.fetch;
+
+ const newRepo = {
+ id: Date.now().toString(),
+ localPath: localPath,
+ remoteUrl: remoteUrl
+ };
+
+ const data = fs.readFileSync(REPOS_FILE);
+ const repos = JSON.parse(data);
+ repos.push(newRepo);
+ fs.writeFileSync(REPOS_FILE, JSON.stringify(repos, null, 2));
+ res.status(201).send(newRepo);
+ } catch (error) {
+ res.status(500).send({ message: `Error processing repository file: ${error.message}` });
+ }
+});
+
+// Remove a repository
+app.delete('/api/repositories', (req, res) => {
+ try {
+ const repoToRemovePath = req.body.path;
+ const data = fs.readFileSync(REPOS_FILE);
+ let repos = JSON.parse(data);
+ repos = repos.filter(repo => repo.localPath !== repoToRemovePath);
+ fs.writeFileSync(REPOS_FILE, JSON.stringify(repos, null, 2));
+ res.status(200).send({ message: 'Repository removed.' });
+ } catch (error) {
+ res.status(500).send({ message: 'Error processing repository file.' });
+ }
+});
+
+// Pull a repository
+app.post('/api/pull', async (req, res) => {
+ const repoPath = req.body.path;
+ try {
+ const git = simpleGit(repoPath);
+ const pullResult = await git.pull();
+ res.send({ output: JSON.stringify(pullResult, null, 2) });
+ } catch (error) {
+ res.status(500).send({ output: `Error: ${error.message}` });
+ }
+});
+
+// Webhook for automatic pull
+app.post('/api/webhook', async (req, res) => {
+ const secret = req.headers['x-webhook-secret'];
+ if (secret !== WEBHOOK_SECRET) {
+ return res.status(401).send('Unauthorized');
+ }
+
+ // Extract repository URLs from the payload (for GitHub)
+ const { git_url, ssh_url, clone_url } = req.body.repository || {};
+ if (!git_url && !ssh_url && !clone_url) {
+ return res.status(400).send('Repository URL not found in webhook payload');
+ }
+
+ try {
+ const data = fs.readFileSync(REPOS_FILE);
+ const repos = JSON.parse(data);
+ const repo = repos.find(r => r.remoteUrl === git_url || r.remoteUrl === ssh_url || r.remoteUrl === clone_url);
+
+ if (repo) {
+ const git = simpleGit(repo.localPath);
+ await git.pull();
+ console.log(`Webhook pull for ${repo.localPath} successful.`);
+ res.status(200).send('Webhook processed successfully');
+ } else {
+ res.status(404).send('Repository not found');
+ }
+ } catch (error) {
+ console.error(`Webhook processing error: ${error.message}`);
+ res.status(500).send('Internal server error');
+ }
+});
+
+app.listen(port, () => {
+ console.log(`Server listening at http://localhost:${port}`);
+});
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 50d3bb6..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-
-import React, { useState, useEffect, useRef } from 'react';
-import { Repo, LogEntry } from './types';
-import { Dashboard } from './Dashboard';
-import { Settings } from './Settings';
-import { Sidebar } from './components/Sidebar';
-import './index.css';
-
-const INITIAL_REPOS: Repo[] = [
- { id: '1', path: 'C:\\Dev\\production-backend', name: 'production-backend', autoPull: true, status: 'clean', lastCheck: '12:00:00' },
- { id: '2', path: 'C:\\Dev\\frontend-client', name: 'frontend-client', autoPull: false, status: 'behind', commitsBehind: 3, lastCheck: '11:55:00' },
-];
-
-const App = () => {
- const [activeTab, setActiveTab] = useState<'dashboard' | 'settings'>('dashboard');
- const [repos, setRepos] = useState(INITIAL_REPOS);
- const [logs, setLogs] = useState([]);
- const [isSimulating, setIsSimulating] = useState(true);
-
- const logsEndRef = useRef(null);
-
- const addLog = (msg: string, type: LogEntry['type'] = 'info') => {
- const entry: LogEntry = {
- id: Date.now() + Math.random(),
- timestamp: new Date().toLocaleTimeString('en-GB'),
- message: msg,
- type
- };
- setLogs(prev => [...prev.slice(-99), entry]);
- };
-
- useEffect(() => {
- logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- }, [logs]);
-
- // --- Real Data & Events ---
- useEffect(() => {
- if (window.electronAPI) {
- // 1. Load persisted repos
- window.electronAPI.getRepos().then((savedRepos: Repo[]) => {
- if (savedRepos && savedRepos.length > 0) {
- setRepos(savedRepos);
- setIsSimulating(false); // Stop simulation if we have real data
- addLog('Loaded repositories from storage', 'info');
- }
- });
-
- // 2. Listen for backend updates
- const cleanup = window.electronAPI.onRepoUpdate((data) => {
- updateRepoStatus(data.id, data.status as any, data.commits);
- if (data.status === 'behind') {
- addLog(`[${data.id}] ${data.commits} new commits available`, 'warn');
- } else if (data.status === 'clean') {
- addLog(`[${data.id}] Updated and clean`, 'success');
- }
- });
- return cleanup;
- }
- }, []);
-
- // --- Simulation Logic ---
- useEffect(() => {
- if (!isSimulating) return;
-
- const interval = setInterval(() => {
- if (repos.length === 0) return;
- const repoIdx = Math.floor(Math.random() * repos.length);
- const repo = repos[repoIdx];
-
- if (repo.status === 'pulling' || repo.status === 'checking') return;
-
- updateRepoStatus(repo.id, 'checking');
- addLog(`[${repo.name}] Checking for updates...`, 'info');
-
- setTimeout(() => {
- const rand = Math.random();
-
- if (rand > 0.7) {
- const behindCount = Math.floor(Math.random() * 5) + 1;
- addLog(`[${repo.name}] Found ${behindCount} new commits.`, 'warn');
-
- if (repo.autoPull) {
- updateRepoStatus(repo.id, 'pulling');
- addLog(`[${repo.name}] Auto-pulling changes...`, 'info');
-
- setTimeout(() => {
- updateRepoStatus(repo.id, 'clean', 0);
- addLog(`[${repo.name}] Successfully pulled updates.`, 'success');
- }, 2000);
- } else {
- updateRepoStatus(repo.id, 'behind', behindCount);
- }
- } else if (rand > 0.95) {
- updateRepoStatus(repo.id, 'error');
- addLog(`[${repo.name}] Error: Network timeout.`, 'error');
- } else {
- updateRepoStatus(repo.id, 'clean');
- }
- }, 1500);
-
- }, 4000);
-
- return () => clearInterval(interval);
- }, [repos, isSimulating]);
-
- const updateRepoStatus = (id: string, status: Repo['status'], commitsBehind?: number) => {
- setRepos(prev => prev.map(r => r.id === id ? {
- ...r,
- status,
- lastCheck: new Date().toLocaleTimeString('en-GB'),
- commitsBehind: commitsBehind !== undefined ? commitsBehind : r.commitsBehind
- } : r));
- };
-
- const toggleAutoPull = (id: string) => {
- setRepos(prev => prev.map(r => {
- if (r.id === id) {
- const newVal = !r.autoPull;
- addLog(`[${r.name}] Auto-pull set to ${newVal ? 'ON' : 'OFF'}`, 'info');
- if (window.electronAPI) window.electronAPI.toggleAutoPull(id);
- return { ...r, autoPull: newVal };
- }
- return r;
- }));
- };
-
- const handleManualPull = (id: string) => {
- const r = repos.find(r => r.id === id);
- if (!r) return;
- updateRepoStatus(id, 'pulling');
- addLog(`[${r.name}] Manual pull requested...`, 'info');
- setTimeout(() => {
- updateRepoStatus(id, 'clean', 0);
- addLog(`[${r.name}] Manual pull successful.`, 'success');
- }, 2000);
- };
-
- const handleAddRepo = async () => {
- // If not in Electron, use fake logic
- if (!window.electronAPI) {
- const name = `new-project-${Math.floor(Math.random() * 1000)}`;
- const newRepo: Repo = {
- id: Date.now().toString(),
- path: `C:\\Projects\\${name}`,
- name: name,
- autoPull: true,
- status: 'checking',
- lastCheck: 'Now'
- };
- setRepos([...repos, newRepo]);
- addLog(`Added new repository: ${name}`, 'info');
- return;
- }
-
- // Native Dialog Flow
- try {
- const result = await window.electronAPI.selectFolder();
- if (!result) return; // Cancelled
-
- if (!result.isRepo) {
- addLog(`Error: Folder '${result.name}' is not a valid Git repository`, 'error');
- // You might want to show a toast or alert here
- return;
- }
-
- const newRepo: Repo = {
- id: Date.now().toString(),
- path: result.path,
- name: result.name,
- autoPull: false,
- status: 'clean',
- lastCheck: 'Just added'
- };
-
- await window.electronAPI.addRepo(newRepo);
- setRepos(prev => [...prev, newRepo]);
- addLog(`Repository added: ${result.name}`, 'success');
-
- // Stop simulation to avoid confusion
- setIsSimulating(false);
- } catch (e) {
- console.error(e);
- addLog('Failed to add repository', 'error');
- }
- };
-
- return (
-
- {/* Custom Title Bar */}
-
-
-
-
window.electronAPI?.minimize()}>─
-
window.electronAPI?.maximize()}>□
-
window.electronAPI?.close()}>✕
-
-
-
-
-
-
-
- {activeTab === 'dashboard' && (
-
- )}
- {activeTab === 'settings' && }
-
-
-
- );
-};
-
-export default App;
diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx
deleted file mode 100644
index e9d323d..0000000
--- a/src/Dashboard.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import React from 'react';
-import { Repo, LogEntry } from './types';
-
-interface DashboardProps {
- repos: Repo[];
- logs: LogEntry[];
- logsEndRef: React.RefObject;
- onToggleAutoPull: (id: string) => void;
- onManualPull: (id: string) => void;
- onAddRepo: () => void;
-}
-
-export const Dashboard: React.FC = ({
- repos,
- logs,
- logsEndRef,
- onToggleAutoPull,
- onManualPull,
- onAddRepo
-}) => (
-
- {/* Repos Section */}
-
-
-
Watched Repositories ({repos.length})
-
-
-
-
- {repos.map((repo) => (
- onToggleAutoPull(repo.id)}
- onPull={() => onManualPull(repo.id)}
- />
- ))}
-
-
-
- {/* Timeline Logs Section */}
-
-
- Activity Timeline
- Clear Logs
-
-
- {logs.length === 0 && (
-
- No activity recorded yet...
-
- )}
- {logs.map((log) => (
-
- ))}
-
-
-
-
-);
-
-// --- Sub-components ---
-
-const RepoCard = ({ repo, onToggle, onPull }: { repo: Repo, onToggle: () => void, onPull: () => void }) => {
- const isProcessing = repo.status === 'pulling' || repo.status === 'checking';
-
- const formatPath = (path: string) => {
- const parts = path.split(/[\\/]/);
- if (parts.length > 2) {
- return `...\\${parts[parts.length - 2]}\\${parts[parts.length - 1]}`;
- }
- return path;
- };
-
- const handleOpenFolder = () => {
- window.electronAPI?.openRepoFolder(repo.path);
- };
-
- const handleOpenUrl = async () => {
- const success = await window.electronAPI?.openRepoUrl(repo.id);
- if (!success) {
- alert('Could not find a remote URL for this repository.');
- }
- };
-
- const handleOpenCode = () => {
- window.electronAPI?.openInVsCode(repo.path);
- };
-
- return (
-
-
-
-
- {repo.name}
-
-
-
{formatPath(repo.path)}
-
-
- {/* Quick Actions Toolbar */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Last checked: {repo.lastCheck}
-
-
-
- );
-};
-
-const StatusBadge = ({ status, count }: { status: string, count?: number }) => {
- switch (status) {
- case 'clean': return Up to date;
- case 'behind': return {count} commits behind;
- case 'error': return Connection Error;
- case 'checking': return Checking...;
- case 'pulling': return Pulling...;
- default: return null;
- }
-};
-
-const TimelineItem = ({ log }: { log: LogEntry }) => {
- const getIcon = (type: string) => {
- switch (type) {
- case 'success': return '✓';
- case 'error': return '✕';
- case 'warn': return '!';
- default: return 'i';
- }
- };
-
- return (
-
-
- {getIcon(log.type)}
-
-
-
{log.message}
-
- {log.timestamp}
-
-
-
- );
-};
\ No newline at end of file
diff --git a/src/Settings.tsx b/src/Settings.tsx
deleted file mode 100644
index c19dbc2..0000000
--- a/src/Settings.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import React, { useState, useEffect } from 'react';
-
-export const Settings = () => {
- const [githubToken, setGithubToken] = useState('');
- const [checkInterval, setCheckInterval] = useState(60);
- const [launchAtStartup, setLaunchAtStartup] = useState(true);
-
- const [isConnecting, setIsConnecting] = useState(false);
- const [isSmartWaiting, setIsSmartWaiting] = useState(false);
- const [connectionStatus, setConnectionStatus] = useState<'idle' | 'success' | 'error'>('idle');
- const [saveStatus, setSaveStatus] = useState('');
-
- // Load Settings on Mount
- useEffect(() => {
- if (window.electronAPI) {
- window.electronAPI.getSettings().then((settings) => {
- if (settings) {
- setCheckInterval(settings.checkInterval || 60);
- setGithubToken(settings.githubToken || '');
- setLaunchAtStartup(settings.launchAtStartup !== false);
- }
- });
-
- // Also listen for smart auth updates
- const cleanup = window.electronAPI.onSmartAuthTokenFound((token) => {
- setGithubToken(token);
- setIsSmartWaiting(false);
- setConnectionStatus('success');
- handleSave({ githubToken: token });
- });
- return cleanup;
- }
- }, []);
-
- const handleSave = async (override: any = {}) => {
- setSaveStatus('Saving...');
- const settings = {
- checkInterval: parseInt(checkInterval.toString()),
- githubToken,
- launchAtStartup,
- ...override
- };
-
- if (window.electronAPI) {
- await window.electronAPI.saveSettings(settings);
- setSaveStatus('Saved!');
- setTimeout(() => setSaveStatus(''), 2000);
- }
- };
-
- const handleConnect = () => {
- if (!githubToken) return;
- setIsConnecting(true);
- setConnectionStatus('idle');
-
- setTimeout(() => {
- setIsConnecting(false);
- // Basic client-side validation logic
- if (githubToken.startsWith('ghp_') || githubToken.startsWith('github_pat_')) {
- setConnectionStatus('success');
- handleSave();
- } else {
- setConnectionStatus('error');
- }
- setTimeout(() => setConnectionStatus('idle'), 3000);
- }, 1000);
- };
-
- const startSmartAuth = () => {
- setIsSmartWaiting(true);
- window.electronAPI?.startSmartAuth();
- };
-
- return (
-
-
Settings
-
-
-
General
-
-
-
- setCheckInterval(parseInt(e.target.value))}
- onBlur={() => handleSave()}
- style={{ background: '#2d2d30', border: '1px solid #444', color: 'white', padding: '6px', borderRadius: '4px', width: '80px' }}
- />
-
-
-
-
-
-
-
- {/* GitHub Authentication Section */}
-
-
GitHub Authentication
-
-
-
-
-
- Connect your GitHub account to access private repositories.
-
-
- {/* Smart Auth Button */}
-
-
-
- {isSmartWaiting && (
-
- Browser opened. Generate the token and just copy it to your clipboard. We'll detect it automatically!
-
- )}
-
-
-
-
-
- setGithubToken(e.target.value)}
- style={{ flex: 1, background: '#1e1e1e', border: connectionStatus === 'error' ? '1px solid #f14c4c' : '1px solid #444', color: 'white', padding: '8px', borderRadius: '4px', fontFamily: 'monospace' }}
- />
-
-
-
-
-
-
-
-
-
-
-
- {saveStatus && {saveStatus}}
-
-
- );
-};
\ No newline at end of file
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
deleted file mode 100644
index 2ed83d5..0000000
--- a/src/components/Sidebar.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-
-import React from 'react';
-
-interface SidebarItemProps {
- active: boolean;
- onClick: () => void;
- icon: string;
- label: string;
-}
-
-export const SidebarItem: React.FC = ({ active, onClick, icon, label }) => (
-
- {icon} {label}
-
-);
-
-export const Sidebar = ({ activeTab, setActiveTab }: { activeTab: string, setActiveTab: (t: 'dashboard'|'settings') => void }) => (
-
-
- setActiveTab('dashboard')}
- icon="📊"
- label="Dashboard"
- />
- setActiveTab('settings')}
- icon="⚙️"
- label="Settings"
- />
-
-
-
-);
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index 4fc70f2..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,158 +0,0 @@
-
-/* Reset & Base */
-:root {
- --bg-app: #202020;
- --bg-panel: #2b2b2b;
- --border-color: #383838;
- --accent: #60cdff; /* Windows 11 Blue */
- --text-primary: #ffffff;
- --text-secondary: #a0a0a0;
- --success: #6cc970;
- --warning: #eac54f;
- --error: #ff6b6b;
-}
-
-* { box-sizing: border-box; user-select: none; }
-body { margin: 0; font-family: 'Segoe UI Variable', 'Segoe UI', sans-serif; background: var(--bg-app); color: var(--text-primary); overflow: hidden; }
-
-/* Layout */
-.app-container { display: flex; flex-direction: column; height: 100vh; }
-.main-layout { display: flex; flex: 1; overflow: hidden; }
-
-/* Title Bar */
-.title-bar {
- height: 38px;
- background: var(--bg-app);
- display: flex; justify-content: space-between;
- align-items: center;
- -webkit-app-region: drag;
- border-bottom: 1px solid var(--border-color);
-}
-.title-drag-region { display: flex; align-items: center; padding-left: 16px; gap: 10px; font-size: 13px; font-weight: 500; }
-.app-icon { font-size: 16px; }
-.window-controls { display: flex; -webkit-app-region: no-drag; height: 100%; }
-.control { width: 46px; display: flex; justify-content: center; align-items: center; cursor: pointer; font-size: 10px; transition: 0.1s; }
-.control:hover { background: #3a3a3a; }
-.control.close:hover { background: #c42b1c; color: white; }
-
-/* Sidebar (Glassmorphismish) */
-.sidebar {
- width: 240px;
- background: rgba(32, 32, 32, 0.95);
- display: flex; flex-direction: column;
- border-right: 1px solid var(--border-color);
- padding-top: 10px;
-}
-.sidebar-menu { padding: 10px; display: flex; flex-direction: column; gap: 4px; }
-.sidebar-footer { margin-top: auto; padding: 20px; font-size: 11px; color: var(--text-secondary); border-top: 1px solid var(--border-color); }
-.status-indicator { display: flex; align-items: center; gap: 8px; color: var(--success); margin-bottom: 4px; font-weight: 500; }
-.dot { width: 8px; height: 8px; background: currentColor; border-radius: 50%; box-shadow: 0 0 8px rgba(108, 201, 112, 0.4); }
-
-/* Content Area */
-.content-area { flex: 1; display: flex; flex-direction: column; background: #191919; overflow: hidden; position: relative; }
-
-/* Dashboard */
-.dashboard-container { display: flex; flex-direction: column; height: 100%; padding: 24px; gap: 24px; }
-.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
-.section-title { margin: 0; font-size: 16px; font-weight: 600; }
-
-.repo-grid { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; padding-right: 8px; flex: 1; }
-
-/* Repo Card */
-.repo-card {
- background: var(--bg-panel);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- padding: 16px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- transition: border-color 0.2s, transform 0.1s;
-}
-.repo-card:hover { border-color: #555; }
-.repo-info h4 { margin: 0 0 6px 0; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
-.repo-path { font-size: 12px; color: var(--text-secondary); font-family: 'Consolas', monospace; opacity: 0.8; cursor: help; }
-
-/* Badges */
-.badge { font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
-.badge.clean { background: rgba(108, 201, 112, 0.15); color: var(--success); border: 1px solid rgba(108, 201, 112, 0.3); }
-.badge.behind { background: rgba(234, 197, 79, 0.15); color: var(--warning); border: 1px solid rgba(234, 197, 79, 0.3); }
-.badge.error { background: rgba(255, 107, 107, 0.15); color: var(--error); border: 1px solid rgba(255, 107, 107, 0.3); }
-.badge.process { background: rgba(96, 205, 255, 0.15); color: var(--accent); border: 1px solid rgba(96, 205, 255, 0.3); animation: pulse 2s infinite; }
-
-@keyframes pulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
-
-/* Quick Actions */
-.quick-actions { display: flex; gap: 6px; margin-top: 10px; }
-.action-btn {
- background: rgba(255,255,255,0.05);
- border: 1px solid transparent;
- color: var(--text-secondary);
- border-radius: 4px;
- padding: 4px 8px;
- font-size: 11px;
- cursor: pointer;
- display: flex; align-items: center; gap: 6px;
-}
-.action-btn:hover { background: rgba(255,255,255,0.1); color: white; border-color: #444; }
-
-/* Timeline Logs */
-.logs-container {
- background: #151515;
- border-top: 1px solid var(--border-color);
- display: flex; flex-direction: column;
- height: 35%; /* Fixed height for log area */
- min-height: 200px;
-}
-.logs-header { background: #1f1f1f; padding: 8px 16px; font-size: 12px; font-weight: 600; color: var(--text-secondary); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; }
-.logs-scroll-area { overflow-y: auto; padding: 16px; flex: 1; font-family: 'Segoe UI', sans-serif; }
-
-.timeline-item { display: flex; gap: 14px; position: relative; padding-bottom: 16px; }
-.timeline-item::before {
- content: ''; position: absolute; left: 9px; top: 24px; bottom: 0; width: 2px; background: #333;
-}
-.timeline-item:last-child::before { display: none; }
-
-.timeline-icon {
- width: 20px; height: 20px; border-radius: 50%;
- display: flex; align-items: center; justify-content: center;
- font-size: 10px; z-index: 2; margin-top: 2px;
- flex-shrink: 0;
-}
-.timeline-icon.info { background: #2b2b2b; color: #aaa; border: 2px solid #333; }
-.timeline-icon.success { background: rgba(108, 201, 112, 0.2); color: var(--success); border: 2px solid var(--success); }
-.timeline-icon.error { background: rgba(255, 107, 107, 0.2); color: var(--error); border: 2px solid var(--error); }
-.timeline-icon.warn { background: rgba(234, 197, 79, 0.2); color: var(--warning); border: 2px solid var(--warning); }
-
-.timeline-content { display: flex; flex-direction: column; gap: 2px; }
-.timeline-msg { font-size: 13px; color: #eee; line-height: 1.4; }
-.timeline-meta { font-size: 11px; color: #666; display: flex; gap: 10px; }
-.timeline-repo { color: var(--accent); font-weight: 500; }
-
-/* Buttons */
-.btn-primary {
- background: var(--accent); color: #000;
- border: none; padding: 6px 14px; border-radius: 6px;
- cursor: pointer; font-weight: 600; font-size: 13px;
- transition: opacity 0.2s;
-}
-.btn-primary:hover { opacity: 0.9; }
-.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
-
-.pull-btn {
- background: transparent;
- border: 1px solid var(--border-color);
- color: white;
- padding: 6px 16px;
- border-radius: 6px;
- cursor: pointer;
- font-size: 13px;
- transition: all 0.2s;
-}
-.pull-btn:hover:not(:disabled) { background: #333; border-color: #666; }
-.pull-btn:disabled { opacity: 0.5; cursor: default; }
-
-/* Settings */
-.settings-container { padding: 30px; max-width: 600px; overflow-y: auto; height: 100%; }
-.settings-group { margin-bottom: 30px; }
-.settings-group h4 { border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; color: var(--accent); font-weight: 600; }
diff --git a/src/main.tsx b/src/main.tsx
deleted file mode 100644
index bad27e2..0000000
--- a/src/main.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-import './index.css'
-
-ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
-
-
- ,
-)
diff --git a/src/types.ts b/src/types.ts
deleted file mode 100644
index bb56052..0000000
--- a/src/types.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-
-export interface Repo {
- id: string;
- path: string;
- name: string;
- autoPull: boolean;
- status: 'clean' | 'behind' | 'error' | 'pulling' | 'checking';
- lastCheck: string;
- commitsBehind?: number;
- githubUrl?: string; // For the "Open in Browser" quick action
-}
-
-export interface LogEntry {
- id: number;
- timestamp: string;
- message: string;
- type: 'info' | 'success' | 'error' | 'warn';
- repoName?: string; // Optional linkage to a specific repo
-}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
deleted file mode 100644
index 7a4d5e9..0000000
--- a/src/vite-env.d.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-///
-
-interface Window {
- electronAPI: {
- getRepos: () => Promise;
- addRepo: (repo: any) => Promise;
- toggleAutoPull: (id: string) => Promise;
- saveGitHubToken: (token: string) => Promise;
-
- // Settings
- getSettings: () => Promise<{ checkInterval: number; githubToken: string; launchAtStartup: boolean }>;
- saveSettings: (settings: { checkInterval: number; launchAtStartup: boolean }) => Promise;
-
- startSmartAuth: () => Promise;
- onSmartAuthTokenFound: (callback: (token: string) => void) => () => void;
-
- onRepoUpdate: (callback: (data: { id: string, status: string, commits: number, githubUrl?: string }) => void) => () => void;
-
- selectFolder: () => Promise<{ path: string; name: string; isRepo: boolean } | null>;
-
- // External Actions
- openRepoFolder: (path: string) => Promise;
- openRepoUrl: (id: string) => Promise;
- openInVsCode: (path: string) => Promise;
-
- minimize: () => void;
- maximize: () => void;
- close: () => void;
- }
-}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 05a07d3..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "compilerOptions": {
- "target": "ESNext",
- "useDefineForClassFields": true,
- "lib": ["ESNext", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "noEmit": true,
- "jsx": "react-jsx",
- "esModuleInterop": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
- },
- "include": ["src", "electron"],
- "references": [{ "path": "./tsconfig.node.json" }]
-}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
deleted file mode 100644
index 099658c..0000000
--- a/tsconfig.node.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "skipLibCheck": true,
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true
- },
- "include": ["vite.config.ts"]
-}
\ No newline at end of file
diff --git a/types.ts b/types.ts
deleted file mode 100644
index 04505bb..0000000
--- a/types.ts
+++ /dev/null
@@ -1 +0,0 @@
-// MOVED TO src/types.ts
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
deleted file mode 100644
index 9ccfd8a..0000000
--- a/vite.config.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { defineConfig } from 'vite'
-import path from 'node:path'
-import { fileURLToPath } from 'node:url'
-import react from '@vitejs/plugin-react'
-import electron from 'vite-plugin-electron/simple'
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url))
-
-// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [
- react(),
- electron({
- main: {
- entry: 'electron/main.ts',
- vite: {
- build: {
- rollupOptions: {
- output: {
- format: 'cjs',
- entryFileNames: '[name].cjs',
- },
- },
- },
- },
- },
- preload: {
- input: 'electron/preload.ts',
- vite: {
- build: {
- rollupOptions: {
- output: {
- format: 'cjs',
- entryFileNames: '[name].cjs',
- },
- },
- },
- },
- },
- renderer: {},
- }),
- ],
-})
\ No newline at end of file