diff --git a/backend/package-lock.json b/backend/package-lock.json index 34144b4..1f2a448 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,12 +13,28 @@ "dotenv": "^17.4.2", "express": "^5.2.1", "mongoose": "^9.7.4", + "simple-git": "^3.36.0", "socket.io": "^4.8.3" }, "devDependencies": { "nodemon": "^3.1.14" } }, + "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/@mongodb-js/saslprep": { "version": "1.4.12", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz", @@ -28,6 +44,21 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -1406,6 +1437,23 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 90f0d6d..9c0f1e0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,6 +16,7 @@ "dotenv": "^17.4.2", "express": "^5.2.1", "mongoose": "^9.7.4", + "simple-git": "^3.36.0", "socket.io": "^4.8.3" }, "devDependencies": { diff --git a/backend/src/models/Site.js b/backend/src/models/Site.js index e02c929..36afb32 100644 --- a/backend/src/models/Site.js +++ b/backend/src/models/Site.js @@ -9,6 +9,8 @@ const siteSchema = new mongoose.Schema( themeColor: { type: String, default: "#2563eb" }, targetWindow: { type: String, default: "_blank", enum: ["_blank", "_self"] }, enabled: { type: Boolean, default: true }, + gitRepoUrl: { type: String, default: "", trim: true }, + gitBranch: { type: String, default: "main", trim: true }, }, { timestamps: true } ); diff --git a/backend/src/routes/sites.js b/backend/src/routes/sites.js index 531f663..8a7c44b 100644 --- a/backend/src/routes/sites.js +++ b/backend/src/routes/sites.js @@ -1,8 +1,17 @@ const express = require("express"); const Site = require("../models/Site"); +const fs = require("fs"); +const path = require("path"); +const simpleGit = require("simple-git"); const router = express.Router(); +const SITES_GIT_DIR = path.join(__dirname, "../../sites-git-data"); + +if (!fs.existsSync(SITES_GIT_DIR)) { + fs.mkdirSync(SITES_GIT_DIR, { recursive: true }); +} + // GET /api/sites - List all sites router.get("/", async (req, res) => { try { @@ -16,8 +25,8 @@ router.get("/", async (req, res) => { // POST /api/sites - Create a new site router.post("/", async (req, res) => { try { - const { id, name, url, description, themeColor, targetWindow, enabled } = req.body; - const site = new Site({ id, name, url, description, themeColor, targetWindow, enabled }); + const { id, name, url, description, themeColor, targetWindow, enabled, gitRepoUrl, gitBranch } = req.body; + const site = new Site({ id, name, url, description, themeColor, targetWindow, enabled, gitRepoUrl: gitRepoUrl || "", gitBranch: gitBranch || "main" }); await site.save(); res.status(201).json(site); } catch (err) { @@ -28,9 +37,10 @@ router.post("/", async (req, res) => { // PUT /api/sites/:id - Update a site router.put("/:id", async (req, res) => { try { - const { name, url, description, themeColor, targetWindow, enabled } = req.body; - const updates = { name, url, description, themeColor, targetWindow, enabled }; - const site = await Site.findOneAndUpdate({ id: req.params.id }, updates, { new: true, runValidators: true }); + const { name, url, description, themeColor, targetWindow, enabled, gitRepoUrl, gitBranch } = req.body; + console.log("[PUT /sites] id:", req.params.id, "body:", JSON.stringify({name,url,description,themeColor,targetWindow,enabled,gitRepoUrl,gitBranch})); + const updates = { name, url, description, themeColor, targetWindow, enabled, gitRepoUrl: gitRepoUrl || "", gitBranch: gitBranch || "main" }; + const site = await Site.findOneAndUpdate({ id: req.params.id }, { $set: updates }, { new: true, runValidators: true }); if (!site) return res.status(404).json({ error: "Site not found" }); res.json(site); } catch (err) { @@ -43,10 +53,116 @@ router.delete("/:id", async (req, res) => { try { const site = await Site.findOneAndDelete({ id: req.params.id }); if (!site) return res.status(404).json({ error: "Site not found" }); + + const siteGitDir = path.join(SITES_GIT_DIR, site.id); + if (fs.existsSync(siteGitDir)) { + fs.rmSync(siteGitDir, { recursive: true, force: true }); + } + res.json({ message: "Site deleted", id: site.id }); } catch (err) { res.status(500).json({ error: err.message }); } }); +// GET /api/sites/:id/git-log - Get git commit log for a site's repository +router.get("/:id/git-log", async (req, res) => { + try { + const site = await Site.findOne({ id: req.params.id }); + if (!site) return res.status(404).json({ error: "Site not found" }); + if (!site.gitRepoUrl) return res.status(400).json({ error: "No git repo URL configured" }); + + const siteGitDir = path.join(SITES_GIT_DIR, site.id); + try { + const git = simpleGit(siteGitDir); + const logs = await git.log(); + res.json({ commits: logs.all || [] }); + } catch (gitErr) { + res.status(500).json({ error: "Failed to read git log. The repository may need to be cloned first. Use the sync endpoint." }); + } + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// POST /api/sites/:id/sync-git - Clone or pull the git repository for a site +router.post("/:id/sync-git", async (req, res) => { + try { + const site = await Site.findOne({ id: req.params.id }); + if (!site) return res.status(404).json({ error: "Site not found" }); + if (!site.gitRepoUrl) return res.status(400).json({ error: "No git repo URL configured" }); + + const siteGitDir = path.join(SITES_GIT_DIR, site.id); + const git = simpleGit(siteGitDir); + + try { + await git.pull(); + res.json({ message: "Repository pulled successfully", commits: (await git.log()).all || [] }); + } catch (pullErr) { + if (pullErr.message.includes("repo does not exist") || pullErr.message.includes("No local restart")) { + const branch = site.gitBranch || "main"; + const gitInstance = simpleGit(); + await gitInstance.clone(site.gitRepoUrl, siteGitDir); + try { + await gitInstance.checkout(branch); + } catch (e) { + // If branch doesn't exist, stay on default branch + } + const logs = await gitInstance.log(); + res.json({ message: "Repository cloned successfully", cloned: true, commits: logs.all || [] }); + } else { + res.status(500).json({ error: pullErr.message }); + } + } + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// GET /api/sites/:id/files - Get file tree for a site's synced repository +router.get("/:id/files", async (req, res) => { + try { + const site = await Site.findOne({ id: req.params.id }); + if (!site) return res.status(404).json({ error: "Site not found" }); + + const siteGitDir = path.join(SITES_GIT_DIR, site.id); + if (!fs.existsSync(siteGitDir)) { + return res.json({ tree: [], message: "Repository not synced yet. Use Sync Git first." }); + } + + function buildTree(dir, relativePath = "") { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const result = []; + + // Folders first, then files + const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)); + const files = entries.filter(e => !e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)); + + for (const d of dirs) { + result.push({ + name: d.name, + path: relativePath ? `${relativePath}/${d.name}` : d.name, + type: "folder", + children: buildTree(path.join(dir, d.name), relativePath ? `${relativePath}/${d.name}` : d.name), + }); + } + + for (const f of files) { + result.push({ + name: f.name, + path: relativePath ? `${relativePath}/${f.name}` : f.name, + type: "file", + }); + } + + return result; + } + + const tree = buildTree(siteGitDir); + res.json({ tree }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + module.exports = router; diff --git a/publisher/.gitignore b/frontend/.gitignore similarity index 100% rename from publisher/.gitignore rename to frontend/.gitignore diff --git a/publisher/README.md b/frontend/README.md similarity index 100% rename from publisher/README.md rename to frontend/README.md diff --git a/publisher/app/app.vue b/frontend/app/app.vue similarity index 73% rename from publisher/app/app.vue rename to frontend/app/app.vue index abe8d21..0a06dc6 100644 --- a/publisher/app/app.vue +++ b/frontend/app/app.vue @@ -8,24 +8,34 @@ -