From 34b0d92e8ae98f2fd6607f28e6214964a0fbf9e2 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Thu, 18 Sep 2025 05:10:58 +0000 Subject: [PATCH] Prettier format pending files --- website/electron-main.js | 25 +- website/public/sw.js | 70 +- website/src/monaco-loader.ts | 5 +- website/src/website/index.tsx | 6 +- .../src/website/pages/switch/SwitchPage.tsx | 1337 +++++++++++------ website/src/website/switch/db.ts | 255 ++-- website/src/website/switch/fs.ts | 223 +-- website/src/website/switch/supabase.ts | 5 +- website/src/website/switch/sync.ts | 79 +- website/src/website/switch/templates.ts | 41 +- website/webpack.config.ts | 10 +- 11 files changed, 1330 insertions(+), 726 deletions(-) diff --git a/website/electron-main.js b/website/electron-main.js index 1670430b..ff92f3c2 100644 --- a/website/electron-main.js +++ b/website/electron-main.js @@ -1,14 +1,23 @@ -const { app, BrowserWindow } = require('electron'); -const path = require('path'); +const { app, BrowserWindow } = require("electron"); +const path = require("path"); function createWindow() { - const win = new BrowserWindow({ width: 1280, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true } }); - const startUrl = process.env.SWITCH_URL || 'http://localhost:8080/switch.html'; - win.loadURL(startUrl); + const win = new BrowserWindow({ + width: 1280, + height: 800, + webPreferences: { nodeIntegration: false, contextIsolation: true }, + }); + const startUrl = + process.env.SWITCH_URL || "http://localhost:8080/switch.html"; + win.loadURL(startUrl); } app.whenReady().then(() => { - createWindow(); - app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); + createWindow(); + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); }); -app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); diff --git a/website/public/sw.js b/website/public/sw.js index 0d20aa2e..1e8657d3 100644 --- a/website/public/sw.js +++ b/website/public/sw.js @@ -1,26 +1,54 @@ -const CACHE = 'switch-cache-v1'; +const CACHE = "switch-cache-v1"; const ASSETS = [ - '/', - '/index.html', - '/playground.html', - '/monarch.html', - '/switch.html' + "/", + "/index.html", + "/playground.html", + "/monarch.html", + "/switch.html", ]; -self.addEventListener('install', (e) => { - e.waitUntil(caches.open(CACHE).then(c=>c.addAll(ASSETS)).then(()=>self.skipWaiting())); +self.addEventListener("install", (e) => { + e.waitUntil( + caches + .open(CACHE) + .then((c) => c.addAll(ASSETS)) + .then(() => self.skipWaiting()) + ); }); -self.addEventListener('activate', (e) => { - e.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim())); +self.addEventListener("activate", (e) => { + e.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)) + ) + ) + .then(() => self.clients.claim()) + ); }); -self.addEventListener('fetch', (e) => { - const url = new URL(e.request.url); - if (url.origin === location.origin) { - e.respondWith(caches.match(e.request).then(r=> r || fetch(e.request).then(resp=>{ - if (e.request.method==='GET' && resp.ok && resp.type==='basic') { - const clone = resp.clone(); - caches.open(CACHE).then(c=>c.put(e.request, clone)); - } - return resp; - }).catch(()=>caches.match('/index.html')))); - } +self.addEventListener("fetch", (e) => { + const url = new URL(e.request.url); + if (url.origin === location.origin) { + e.respondWith( + caches.match(e.request).then( + (r) => + r || + fetch(e.request) + .then((resp) => { + if ( + e.request.method === "GET" && + resp.ok && + resp.type === "basic" + ) { + const clone = resp.clone(); + caches + .open(CACHE) + .then((c) => c.put(e.request, clone)); + } + return resp; + }) + .catch(() => caches.match("/index.html")) + ) + ); + } }); diff --git a/website/src/monaco-loader.ts b/website/src/monaco-loader.ts index 08368274..0d8d47bc 100644 --- a/website/src/monaco-loader.ts +++ b/website/src/monaco-loader.ts @@ -93,7 +93,10 @@ async function _loadMonaco(setup: IMonacoSetup): Promise { ); } catch (e) { // If loading optional language contributions fails, still resolve the editor to keep app functional. - console.error('Failed to load Monaco language contributions, continuing without them.', e); + console.error( + "Failed to load Monaco language contributions, continuing without them.", + e + ); res(monaco); } }); diff --git a/website/src/website/index.tsx b/website/src/website/index.tsx index b9de72aa..83e7f3aa 100644 --- a/website/src/website/index.tsx +++ b/website/src/website/index.tsx @@ -9,8 +9,8 @@ elem.className = "root"; document.body.append(elem); ReactDOM.render(, elem); -if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js').catch(() => {}); +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker.register("/sw.js").catch(() => {}); }); } diff --git a/website/src/website/pages/switch/SwitchPage.tsx b/website/src/website/pages/switch/SwitchPage.tsx index 3f9430e4..b8a63367 100644 --- a/website/src/website/pages/switch/SwitchPage.tsx +++ b/website/src/website/pages/switch/SwitchPage.tsx @@ -1,458 +1,901 @@ import * as React from "react"; import { ControlledMonacoEditor } from "../../components/monaco/MonacoEditor"; import "../../switch.scss"; -import { createRepository, listRepositories, listBranches, createBranch } from "../../switch/models"; -import { pickDirectory, getDirectoryHandle, ensureReadPerm, walk } from "../../switch/fs"; -import type { RepoRecord, BranchRecord, CommitRecord, IssueRecord } from "../../switch/db"; +import { + createRepository, + listRepositories, + listBranches, + createBranch, +} from "../../switch/models"; +import { + pickDirectory, + getDirectoryHandle, + ensureReadPerm, + walk, +} from "../../switch/fs"; +import type { + RepoRecord, + BranchRecord, + CommitRecord, + IssueRecord, +} from "../../switch/db"; import { FileTree } from "./FileTree"; -import { getCurrentBranchId, setCurrentBranchId, commitOnBranch, listCommitsReachable, listIssues, createIssue } from "../../switch/logic"; +import { + getCurrentBranchId, + setCurrentBranchId, + commitOnBranch, + listCommitsReachable, + listIssues, + createIssue, +} from "../../switch/logic"; -interface OpenFile { path: string; content: string; language: string } - -export function SwitchPage() { - const [repos, setRepos] = React.useState([]); - const [activeRepo, setActiveRepo] = React.useState(); - const [tree, setTree] = React.useState<{ path: string; isDir: boolean; children?: any[] }[]>([]); - const [openFile, setOpenFile] = React.useState(); - const [editorValue, setEditorValue] = React.useState("// SWITCH: Open a folder to get started\n"); - const [branches, setBranches] = React.useState([]); - const [currentBranchId, setCurrentBranchIdState] = React.useState(undefined); - const [commitMsg, setCommitMsg] = React.useState(""); - const [history, setHistory] = React.useState([]); - const [issues, setIssues] = React.useState([]); - const [newIssueTitle, setNewIssueTitle] = React.useState(""); - const [selectedIssueId, setSelectedIssueId] = React.useState(undefined); - const selectedIssue = issues.find(i=>i.id===selectedIssueId); - const [issueTitle, setIssueTitle] = React.useState(""); - const [issueBody, setIssueBody] = React.useState(""); - const [issueLabels, setIssueLabels] = React.useState(""); - const [issueStatus, setIssueStatus] = React.useState<"open"|"closed">("open"); - - React.useEffect(() => { - refreshRepos(); - }, []); - - React.useEffect(() => { (async () => { - if (!activeRepo) return; - const b = await listBranches(activeRepo.id); - setBranches(b); - const cur = await getCurrentBranchId(activeRepo.id) || b.find(x=>x.name===activeRepo.defaultBranch)?.id; - if (cur) { - setCurrentBranchIdState(cur); - await refreshHistory(activeRepo.id, cur); - } - const iss = await listIssues(activeRepo.id); - setIssues(iss); - if (iss.length) selectIssue(iss[0].id); - })(); }, [activeRepo?.id]); - - function selectIssue(id: string) { - setSelectedIssueId(id); - const it = issues.find(i=>i.id===id); - if (it) { - setIssueTitle(it.title); - setIssueBody(it.body); - setIssueLabels(it.labels.join(", ")); - setIssueStatus(it.status); - } - } - - async function refreshRepos() { - const list = await listRepositories(); - setRepos(list); - } - - async function onOpenFolder() { - const { id: fsId, handle } = await pickDirectory(); - if (!(await ensureReadPerm(handle))) return; - const repo = await createRepository(handle.name || "workspace", fsId); - await refreshRepos(); - setActiveRepo(repo); - await loadTree(repo); - } - - async function loadTree(repo: RepoRecord) { - if (!repo.fsHandleId) return; - const dir = await getDirectoryHandle(repo.fsHandleId); - if (!dir) return; - if (!(await ensureReadPerm(dir))) return; - - // flatten files then build a simple tree - const files: { path: string }[] = []; - for await (const f of walk(dir)) files.push({ path: f.path }); - const root: any = {}; - for (const f of files) { - const parts = f.path.split("/"); - let cur = root; - for (let i = 0; i < parts.length; i++) { - const key = parts.slice(0, i + 1).join("/"); - const isDir = i < parts.length - 1; - cur.children = cur.children || new Map(); - if (!cur.children.has(key)) cur.children.set(key, { path: key, isDir, children: undefined }); - cur = cur.children.get(key); - } - } - const toArray = (node: any): any[] => { - if (!node.children) return []; - const arr = Array.from(node.children.values()); - for (const n of arr) if (n.isDir) n.children = toArray(n); - arr.sort((a, b) => (a.isDir === b.isDir ? a.path.localeCompare(b.path) : a.isDir ? -1 : 1)); - return arr; - }; - setTree(toArray(root)); - } - - async function onSelectRepo(repoId: string) { - const repo = repos.find((r) => r.id === repoId); - setActiveRepo(repo); - if (repo) await loadTree(repo); - } - - async function onOpenFile(path: string) { - if (!activeRepo?.fsHandleId) return; - const dir = await getDirectoryHandle(activeRepo.fsHandleId); - if (!dir) return; - if (!(await ensureReadPerm(dir))) return; - const file = await getFileByPath(dir, path); - if (!file) return; - const content = await file.text(); - setOpenFile({ path, content, language: languageFromPath(path) }); - setEditorValue(content); - } - - async function refreshHistory(repoId: string, branchId: string) { - const b = branches.find(x=>x.id===branchId); - const list = await listCommitsReachable(repoId, b?.headCommitId); - setHistory(list); - } - - return ( -
-
- - - - -
-
- -
-
-
{openFile?.path || "welcome.txt"}
-
- - - - -
-
-
- -
-
-
-
-
SWITCH
-
{openFile?.language || "Plain Text"}
-
UTF-8
-
LF
-
-
- ); - async function onRefresh() { - if (activeRepo) await loadTree(activeRepo); - } - - async function onNewFile() { - if (!activeRepo?.fsHandleId) return; - const name = prompt("New file path (relative):", "untitled.txt"); - if (!name) return; - const dir = await getDirectoryHandle(activeRepo.fsHandleId); - if (!dir) return; - await (await import("../../switch/fs")).writeFileText(dir, name, ""); - await onRefresh(); - } - - async function onNewFolder() { - if (!activeRepo?.fsHandleId) return; - const name = prompt("New folder path (relative):", "folder"); - if (!name) return; - const dir = await getDirectoryHandle(activeRepo.fsHandleId); - if (!dir) return; - await (await import("../../switch/fs")).createDirectory(dir, name); - await onRefresh(); - } - - async function onSave() { - if (!activeRepo?.fsHandleId || !openFile) return; - const dir = await getDirectoryHandle(activeRepo.fsHandleId); - if (!dir) return; - await (await import("../../switch/fs")).writeFileText(dir, openFile.path, editorValue); - } - - async function onSaveAs() { - if (!activeRepo?.fsHandleId) return; - const target = prompt("Save As path (relative):", openFile?.path || "untitled.txt"); - if (!target) return; - const dir = await getDirectoryHandle(activeRepo.fsHandleId); - if (!dir) return; - await (await import("../../switch/fs")).writeFileText(dir, target, editorValue); - setOpenFile({ path: target, content: editorValue, language: languageFromPath(target) }); - await onRefresh(); - } - - async function onSaveRepoAsTemplate() { - if (!activeRepo?.fsHandleId) return; - const name = prompt("Template name:", activeRepo.name + " template"); - if (!name) return; - const { createRepoTemplate } = await import("../../switch/templates"); - await createRepoTemplate(name, activeRepo.fsHandleId); - alert("Template saved."); - } - - async function onCreateRepoFromTemplate() { - const { listRepoTemplates } = await import("../../switch/templates"); - const list = await listRepoTemplates(); - if (!list.length) { alert("No repo templates."); return; } - const names = list.map((t, i)=> `${i+1}. ${t.name}`).join("\n"); - const idxStr = prompt(`Choose template:\n${names}`, "1"); - if (!idxStr) return; const idx = parseInt(idxStr, 10) - 1; if (isNaN(idx) || idx<0 || idx>=list.length) return; - const tpl = list[idx]; - const dest = await pickDirectory(); - const srcHandle = await getDirectoryHandle(tpl.fsHandleId); - if (!srcHandle) { alert("Template folder permission required. Re-save the template."); return; } - await (await import("../../switch/fs")).copyDirectory(srcHandle, dest.handle); - const repo = await createRepository(dest.handle.name || "workspace", dest.id); - await refreshRepos(); - setActiveRepo(repo); - await loadTree(repo); - } - - async function onRename() { - if (!activeRepo?.fsHandleId || !openFile) return; - const next = prompt("Rename to (relative):", openFile.path); - if (!next || next === openFile.path) return; - const dir = await getDirectoryHandle(activeRepo.fsHandleId); - if (!dir) return; - const fs = await import("../../switch/fs"); - await fs.writeFileText(dir, next, editorValue); - await fs.deleteEntry(dir, openFile.path); - setOpenFile({ path: next, content: editorValue, language: languageFromPath(next) }); - await onRefresh(); - } - - async function onDelete() { - if (!activeRepo?.fsHandleId || !openFile) return; - if (!confirm(`Delete ${openFile.path}?`)) return; - const dir = await getDirectoryHandle(activeRepo.fsHandleId); - if (!dir) return; - await (await import("../../switch/fs")).deleteEntry(dir, openFile.path); - setOpenFile(undefined); - setEditorValue("// File deleted\n"); - await onRefresh(); - } - - async function onSaveIssue() { - if (!activeRepo || !selectedIssue) return; - const { updateIssue } = await import("../../switch/logic"); - selectedIssue.title = issueTitle; - selectedIssue.body = issueBody; - selectedIssue.labels = issueLabels.split(",").map(s=>s.trim()).filter(Boolean); - selectedIssue.status = issueStatus; - await updateIssue(selectedIssue); - setIssues(await listIssues(activeRepo.id)); - } - - async function onDeleteIssue() { - if (!activeRepo || !selectedIssue) return; - if (!confirm(`Delete issue ${selectedIssue.title}?`)) return; - const { deleteIssue } = await import("../../switch/logic"); - await deleteIssue(selectedIssue.id); - setIssues(await listIssues(activeRepo.id)); - setSelectedIssueId(undefined); - } - - async function onPushToCloud() { - if (!activeRepo) return; - try { - const { pushRepoToSupabase } = await import("../../switch/sync"); - await pushRepoToSupabase(activeRepo.id); - alert("Pushed to Supabase storage."); - } catch (e:any) { - alert("Cloud push failed: " + (e?.message || e)); - } - } - async function onPullFromCloud() { - if (!activeRepo) return; - try { - const { pullRepoFromSupabase } = await import("../../switch/sync"); - const bundle = await pullRepoFromSupabase(activeRepo.id); - if (!bundle) { alert("No bundle found."); return; } - // For now, just show summary; full import/merge logic can be added as next step - alert(`Fetched bundle exportedAt=${new Date(bundle.exportedAt).toLocaleString()}\nbranches=${bundle.branches.length}, commits=${bundle.commits.length}, issues=${bundle.issues.length}`); - } catch (e:any) { - alert("Cloud pull failed: " + (e?.message || e)); - } - } +interface OpenFile { + path: string; + content: string; + language: string; } -async function getFileByPath(dir: FileSystemDirectoryHandle, path: string): Promise { - const parts = path.split("/"); - let cur: FileSystemDirectoryHandle = dir; - for (let i = 0; i < parts.length; i++) { - const name = parts[i]; - if (i === parts.length - 1) { - const fh = await (cur as any).getFileHandle(name).catch(() => undefined) as FileSystemFileHandle | undefined; - return fh ? fh.getFile() : undefined; - } else { - cur = await (cur as any).getDirectoryHandle(name).catch(() => undefined); - if (!cur) return undefined as any; - } - } - return undefined as any; +export function SwitchPage() { + const [repos, setRepos] = React.useState([]); + const [activeRepo, setActiveRepo] = React.useState< + RepoRecord | undefined + >(); + const [tree, setTree] = React.useState< + { path: string; isDir: boolean; children?: any[] }[] + >([]); + const [openFile, setOpenFile] = React.useState(); + const [editorValue, setEditorValue] = React.useState( + "// SWITCH: Open a folder to get started\n" + ); + const [branches, setBranches] = React.useState([]); + const [currentBranchId, setCurrentBranchIdState] = React.useState< + string | undefined + >(undefined); + const [commitMsg, setCommitMsg] = React.useState(""); + const [history, setHistory] = React.useState([]); + const [issues, setIssues] = React.useState([]); + const [newIssueTitle, setNewIssueTitle] = React.useState(""); + const [selectedIssueId, setSelectedIssueId] = React.useState< + string | undefined + >(undefined); + const selectedIssue = issues.find((i) => i.id === selectedIssueId); + const [issueTitle, setIssueTitle] = React.useState(""); + const [issueBody, setIssueBody] = React.useState(""); + const [issueLabels, setIssueLabels] = React.useState(""); + const [issueStatus, setIssueStatus] = React.useState<"open" | "closed">( + "open" + ); + + React.useEffect(() => { + refreshRepos(); + }, []); + + React.useEffect(() => { + (async () => { + if (!activeRepo) return; + const b = await listBranches(activeRepo.id); + setBranches(b); + const cur = + (await getCurrentBranchId(activeRepo.id)) || + b.find((x) => x.name === activeRepo.defaultBranch)?.id; + if (cur) { + setCurrentBranchIdState(cur); + await refreshHistory(activeRepo.id, cur); + } + const iss = await listIssues(activeRepo.id); + setIssues(iss); + if (iss.length) selectIssue(iss[0].id); + })(); + }, [activeRepo?.id]); + + function selectIssue(id: string) { + setSelectedIssueId(id); + const it = issues.find((i) => i.id === id); + if (it) { + setIssueTitle(it.title); + setIssueBody(it.body); + setIssueLabels(it.labels.join(", ")); + setIssueStatus(it.status); + } + } + + async function refreshRepos() { + const list = await listRepositories(); + setRepos(list); + } + + async function onOpenFolder() { + const { id: fsId, handle } = await pickDirectory(); + if (!(await ensureReadPerm(handle))) return; + const repo = await createRepository(handle.name || "workspace", fsId); + await refreshRepos(); + setActiveRepo(repo); + await loadTree(repo); + } + + async function loadTree(repo: RepoRecord) { + if (!repo.fsHandleId) return; + const dir = await getDirectoryHandle(repo.fsHandleId); + if (!dir) return; + if (!(await ensureReadPerm(dir))) return; + + // flatten files then build a simple tree + const files: { path: string }[] = []; + for await (const f of walk(dir)) files.push({ path: f.path }); + const root: any = {}; + for (const f of files) { + const parts = f.path.split("/"); + let cur = root; + for (let i = 0; i < parts.length; i++) { + const key = parts.slice(0, i + 1).join("/"); + const isDir = i < parts.length - 1; + cur.children = cur.children || new Map(); + if (!cur.children.has(key)) + cur.children.set(key, { + path: key, + isDir, + children: undefined, + }); + cur = cur.children.get(key); + } + } + const toArray = (node: any): any[] => { + if (!node.children) return []; + const arr = Array.from(node.children.values()); + for (const n of arr) if (n.isDir) n.children = toArray(n); + arr.sort((a, b) => + a.isDir === b.isDir + ? a.path.localeCompare(b.path) + : a.isDir + ? -1 + : 1 + ); + return arr; + }; + setTree(toArray(root)); + } + + async function onSelectRepo(repoId: string) { + const repo = repos.find((r) => r.id === repoId); + setActiveRepo(repo); + if (repo) await loadTree(repo); + } + + async function onOpenFile(path: string) { + if (!activeRepo?.fsHandleId) return; + const dir = await getDirectoryHandle(activeRepo.fsHandleId); + if (!dir) return; + if (!(await ensureReadPerm(dir))) return; + const file = await getFileByPath(dir, path); + if (!file) return; + const content = await file.text(); + setOpenFile({ path, content, language: languageFromPath(path) }); + setEditorValue(content); + } + + async function refreshHistory(repoId: string, branchId: string) { + const b = branches.find((x) => x.id === branchId); + const list = await listCommitsReachable(repoId, b?.headCommitId); + setHistory(list); + } + + return ( +
+
+ + + + +
+
+ +
+
+
+ {openFile?.path || "welcome.txt"} +
+
+ + + + +
+
+
+ +
+
+
+
+
SWITCH
+
+ {openFile?.language || "Plain Text"} +
+
UTF-8
+
LF
+
+
+ ); + async function onRefresh() { + if (activeRepo) await loadTree(activeRepo); + } + + async function onNewFile() { + if (!activeRepo?.fsHandleId) return; + const name = prompt("New file path (relative):", "untitled.txt"); + if (!name) return; + const dir = await getDirectoryHandle(activeRepo.fsHandleId); + if (!dir) return; + await (await import("../../switch/fs")).writeFileText(dir, name, ""); + await onRefresh(); + } + + async function onNewFolder() { + if (!activeRepo?.fsHandleId) return; + const name = prompt("New folder path (relative):", "folder"); + if (!name) return; + const dir = await getDirectoryHandle(activeRepo.fsHandleId); + if (!dir) return; + await (await import("../../switch/fs")).createDirectory(dir, name); + await onRefresh(); + } + + async function onSave() { + if (!activeRepo?.fsHandleId || !openFile) return; + const dir = await getDirectoryHandle(activeRepo.fsHandleId); + if (!dir) return; + await ( + await import("../../switch/fs") + ).writeFileText(dir, openFile.path, editorValue); + } + + async function onSaveAs() { + if (!activeRepo?.fsHandleId) return; + const target = prompt( + "Save As path (relative):", + openFile?.path || "untitled.txt" + ); + if (!target) return; + const dir = await getDirectoryHandle(activeRepo.fsHandleId); + if (!dir) return; + await ( + await import("../../switch/fs") + ).writeFileText(dir, target, editorValue); + setOpenFile({ + path: target, + content: editorValue, + language: languageFromPath(target), + }); + await onRefresh(); + } + + async function onSaveRepoAsTemplate() { + if (!activeRepo?.fsHandleId) return; + const name = prompt("Template name:", activeRepo.name + " template"); + if (!name) return; + const { createRepoTemplate } = await import("../../switch/templates"); + await createRepoTemplate(name, activeRepo.fsHandleId); + alert("Template saved."); + } + + async function onCreateRepoFromTemplate() { + const { listRepoTemplates } = await import("../../switch/templates"); + const list = await listRepoTemplates(); + if (!list.length) { + alert("No repo templates."); + return; + } + const names = list.map((t, i) => `${i + 1}. ${t.name}`).join("\n"); + const idxStr = prompt(`Choose template:\n${names}`, "1"); + if (!idxStr) return; + const idx = parseInt(idxStr, 10) - 1; + if (isNaN(idx) || idx < 0 || idx >= list.length) return; + const tpl = list[idx]; + const dest = await pickDirectory(); + const srcHandle = await getDirectoryHandle(tpl.fsHandleId); + if (!srcHandle) { + alert("Template folder permission required. Re-save the template."); + return; + } + await ( + await import("../../switch/fs") + ).copyDirectory(srcHandle, dest.handle); + const repo = await createRepository( + dest.handle.name || "workspace", + dest.id + ); + await refreshRepos(); + setActiveRepo(repo); + await loadTree(repo); + } + + async function onRename() { + if (!activeRepo?.fsHandleId || !openFile) return; + const next = prompt("Rename to (relative):", openFile.path); + if (!next || next === openFile.path) return; + const dir = await getDirectoryHandle(activeRepo.fsHandleId); + if (!dir) return; + const fs = await import("../../switch/fs"); + await fs.writeFileText(dir, next, editorValue); + await fs.deleteEntry(dir, openFile.path); + setOpenFile({ + path: next, + content: editorValue, + language: languageFromPath(next), + }); + await onRefresh(); + } + + async function onDelete() { + if (!activeRepo?.fsHandleId || !openFile) return; + if (!confirm(`Delete ${openFile.path}?`)) return; + const dir = await getDirectoryHandle(activeRepo.fsHandleId); + if (!dir) return; + await (await import("../../switch/fs")).deleteEntry(dir, openFile.path); + setOpenFile(undefined); + setEditorValue("// File deleted\n"); + await onRefresh(); + } + + async function onSaveIssue() { + if (!activeRepo || !selectedIssue) return; + const { updateIssue } = await import("../../switch/logic"); + selectedIssue.title = issueTitle; + selectedIssue.body = issueBody; + selectedIssue.labels = issueLabels + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + selectedIssue.status = issueStatus; + await updateIssue(selectedIssue); + setIssues(await listIssues(activeRepo.id)); + } + + async function onDeleteIssue() { + if (!activeRepo || !selectedIssue) return; + if (!confirm(`Delete issue ${selectedIssue.title}?`)) return; + const { deleteIssue } = await import("../../switch/logic"); + await deleteIssue(selectedIssue.id); + setIssues(await listIssues(activeRepo.id)); + setSelectedIssueId(undefined); + } + + async function onPushToCloud() { + if (!activeRepo) return; + try { + const { pushRepoToSupabase } = await import("../../switch/sync"); + await pushRepoToSupabase(activeRepo.id); + alert("Pushed to Supabase storage."); + } catch (e: any) { + alert("Cloud push failed: " + (e?.message || e)); + } + } + async function onPullFromCloud() { + if (!activeRepo) return; + try { + const { pullRepoFromSupabase } = await import("../../switch/sync"); + const bundle = await pullRepoFromSupabase(activeRepo.id); + if (!bundle) { + alert("No bundle found."); + return; + } + // For now, just show summary; full import/merge logic can be added as next step + alert( + `Fetched bundle exportedAt=${new Date( + bundle.exportedAt + ).toLocaleString()}\nbranches=${ + bundle.branches.length + }, commits=${bundle.commits.length}, issues=${ + bundle.issues.length + }` + ); + } catch (e: any) { + alert("Cloud pull failed: " + (e?.message || e)); + } + } +} + +async function getFileByPath( + dir: FileSystemDirectoryHandle, + path: string +): Promise { + const parts = path.split("/"); + let cur: FileSystemDirectoryHandle = dir; + for (let i = 0; i < parts.length; i++) { + const name = parts[i]; + if (i === parts.length - 1) { + const fh = (await (cur as any) + .getFileHandle(name) + .catch(() => undefined)) as FileSystemFileHandle | undefined; + return fh ? fh.getFile() : undefined; + } else { + cur = await (cur as any) + .getDirectoryHandle(name) + .catch(() => undefined); + if (!cur) return undefined as any; + } + } + return undefined as any; } function languageFromPath(path: string): string { - const ext = path.split(".").pop()?.toLowerCase(); - switch (ext) { - case "ts": return "typescript"; - case "tsx": return "typescript"; - case "js": return "javascript"; - case "jsx": return "javascript"; - case "json": return "json"; - case "css": return "css"; - case "scss": return "scss"; - case "html": return "html"; - case "md": return "markdown"; - case "py": return "python"; - case "rb": return "ruby"; - case "go": return "go"; - case "rs": return "rust"; - case "java": return "java"; - case "cs": return "csharp"; - case "cpp": return "cpp"; - case "yml": - case "yaml": return "yaml"; - case "xml": return "xml"; - case "sql": return "sql"; - default: return "plaintext"; - } + const ext = path.split(".").pop()?.toLowerCase(); + switch (ext) { + case "ts": + return "typescript"; + case "tsx": + return "typescript"; + case "js": + return "javascript"; + case "jsx": + return "javascript"; + case "json": + return "json"; + case "css": + return "css"; + case "scss": + return "scss"; + case "html": + return "html"; + case "md": + return "markdown"; + case "py": + return "python"; + case "rb": + return "ruby"; + case "go": + return "go"; + case "rs": + return "rust"; + case "java": + return "java"; + case "cs": + return "csharp"; + case "cpp": + return "cpp"; + case "yml": + case "yaml": + return "yaml"; + case "xml": + return "xml"; + case "sql": + return "sql"; + default: + return "plaintext"; + } } diff --git a/website/src/website/switch/db.ts b/website/src/website/switch/db.ts index b814badc..4694d25f 100644 --- a/website/src/website/switch/db.ts +++ b/website/src/website/switch/db.ts @@ -1,167 +1,190 @@ -export type StoreName = "repos" | "branches" | "commits" | "issues" | "fsHandles" | "settings"; +export type StoreName = + | "repos" + | "branches" + | "commits" + | "issues" + | "fsHandles" + | "settings"; const DB_NAME = "switch-db"; const DB_VERSION = 2; export interface RepoRecord { - id: string; - name: string; - createdAt: number; - defaultBranch: string; - fsHandleId?: string; + id: string; + name: string; + createdAt: number; + defaultBranch: string; + fsHandleId?: string; } export interface BranchRecord { - id: string; - repoId: string; - name: string; - headCommitId?: string; + id: string; + repoId: string; + name: string; + headCommitId?: string; } export interface CommitRecord { - id: string; - repoId: string; - message: string; - parentIds: string[]; - timestamp: number; + id: string; + repoId: string; + message: string; + parentIds: string[]; + timestamp: number; } export interface IssueRecord { - id: string; - repoId: string; - title: string; - body: string; - labels: string[]; - status: "open" | "closed"; - createdAt: number; - updatedAt: number; - branchId?: string; - filePath?: string; + id: string; + repoId: string; + title: string; + body: string; + labels: string[]; + status: "open" | "closed"; + createdAt: number; + updatedAt: number; + branchId?: string; + filePath?: string; } export interface FsHandleRecord { - id: string; - handle: FileSystemDirectoryHandle; + id: string; + handle: FileSystemDirectoryHandle; } export interface RepoTemplateRecord { - id: string; - name: string; - fsHandleId: string; + id: string; + name: string; + fsHandleId: string; } export interface IssueTemplateRecord { - id: string; - name: string; - title: string; - body: string; - labels: string[]; + id: string; + name: string; + title: string; + body: string; + labels: string[]; } export async function openDB(): Promise { - return new Promise((resolve, reject) => { - const req = indexedDB.open(DB_NAME, DB_VERSION); - req.onupgradeneeded = () => { - const db = req.result; - if (!db.objectStoreNames.contains("repos")) { - const store = db.createObjectStore("repos", { keyPath: "id" }); - store.createIndex("by_name", "name", { unique: false }); - } - if (!db.objectStoreNames.contains("branches")) { - const store = db.createObjectStore("branches", { keyPath: "id" }); - store.createIndex("by_repo", "repoId", { unique: false }); - store.createIndex("by_repo_name", ["repoId", "name"], { unique: true }); - } - if (!db.objectStoreNames.contains("commits")) { - const store = db.createObjectStore("commits", { keyPath: "id" }); - store.createIndex("by_repo", "repoId", { unique: false }); - } - if (!db.objectStoreNames.contains("issues")) { - const store = db.createObjectStore("issues", { keyPath: "id" }); - store.createIndex("by_repo", "repoId", { unique: false }); - } - if (!db.objectStoreNames.contains("fsHandles")) { - db.createObjectStore("fsHandles", { keyPath: "id" }); - } - if (!db.objectStoreNames.contains("settings")) { - db.createObjectStore("settings", { keyPath: "id" }); - } - if (!db.objectStoreNames.contains("repoTemplates")) { - db.createObjectStore("repoTemplates", { keyPath: "id" }); - } - if (!db.objectStoreNames.contains("issueTemplates")) { - db.createObjectStore("issueTemplates", { keyPath: "id" }); - } - }; - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); - }); + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains("repos")) { + const store = db.createObjectStore("repos", { keyPath: "id" }); + store.createIndex("by_name", "name", { unique: false }); + } + if (!db.objectStoreNames.contains("branches")) { + const store = db.createObjectStore("branches", { + keyPath: "id", + }); + store.createIndex("by_repo", "repoId", { unique: false }); + store.createIndex("by_repo_name", ["repoId", "name"], { + unique: true, + }); + } + if (!db.objectStoreNames.contains("commits")) { + const store = db.createObjectStore("commits", { + keyPath: "id", + }); + store.createIndex("by_repo", "repoId", { unique: false }); + } + if (!db.objectStoreNames.contains("issues")) { + const store = db.createObjectStore("issues", { keyPath: "id" }); + store.createIndex("by_repo", "repoId", { unique: false }); + } + if (!db.objectStoreNames.contains("fsHandles")) { + db.createObjectStore("fsHandles", { keyPath: "id" }); + } + if (!db.objectStoreNames.contains("settings")) { + db.createObjectStore("settings", { keyPath: "id" }); + } + if (!db.objectStoreNames.contains("repoTemplates")) { + db.createObjectStore("repoTemplates", { keyPath: "id" }); + } + if (!db.objectStoreNames.contains("issueTemplates")) { + db.createObjectStore("issueTemplates", { keyPath: "id" }); + } + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); } -export async function tx(storeNames: StoreName[], mode: IDBTransactionMode, fn: (tx: IDBTransaction) => Promise): Promise { - const db = await openDB(); - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeNames, mode); - const done = async () => { - try { - const r = await fn(transaction); - resolve(r); - } catch (e) { - reject(e); - } - }; - transaction.oncomplete = () => db.close(); - transaction.onabort = () => reject(transaction.error); - transaction.onerror = () => reject(transaction.error); - done(); - }); +export async function tx( + storeNames: StoreName[], + mode: IDBTransactionMode, + fn: (tx: IDBTransaction) => Promise +): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(storeNames, mode); + const done = async () => { + try { + const r = await fn(transaction); + resolve(r); + } catch (e) { + reject(e); + } + }; + transaction.oncomplete = () => db.close(); + transaction.onabort = () => reject(transaction.error); + transaction.onerror = () => reject(transaction.error); + done(); + }); } export async function put(store: StoreName, value: T): Promise { - await tx([store], "readwrite", async (t) => { - await requestAsPromise(t.objectStore(store).put(value as any)); - return undefined as any; - }); + await tx([store], "readwrite", async (t) => { + await requestAsPromise(t.objectStore(store).put(value as any)); + return undefined as any; + }); } -export async function get(store: StoreName, key: IDBValidKey): Promise { - return tx([store], "readonly", async (t) => { - return requestAsPromise(t.objectStore(store).get(key)); - }); +export async function get( + store: StoreName, + key: IDBValidKey +): Promise { + return tx([store], "readonly", async (t) => { + return requestAsPromise(t.objectStore(store).get(key)); + }); } export async function del(store: StoreName, key: IDBValidKey): Promise { - await tx([store], "readwrite", async (t) => { - await requestAsPromise(t.objectStore(store).delete(key)); - return undefined as any; - }); + await tx([store], "readwrite", async (t) => { + await requestAsPromise(t.objectStore(store).delete(key)); + return undefined as any; + }); } -export async function getAllByIndex(store: StoreName, index: string, query: IDBValidKey | IDBKeyRange): Promise { - return tx([store], "readonly", async (t) => { - const idx = t.objectStore(store).index(index); - return requestAsPromise(idx.getAll(query)); - }); +export async function getAllByIndex( + store: StoreName, + index: string, + query: IDBValidKey | IDBKeyRange +): Promise { + return tx([store], "readonly", async (t) => { + const idx = t.objectStore(store).index(index); + return requestAsPromise(idx.getAll(query)); + }); } export async function getAll(store: StoreName): Promise { - return tx([store], "readonly", async (t) => { - return requestAsPromise(t.objectStore(store).getAll()); - }); + return tx([store], "readonly", async (t) => { + return requestAsPromise(t.objectStore(store).getAll()); + }); } export async function setSetting(id: string, value: T): Promise { - await put("settings", { id, value } as any); + await put("settings", { id, value } as any); } export async function getSetting(id: string): Promise { - const rec = await get("settings", id); - return rec?.value as T | undefined; + const rec = await get("settings", id); + return rec?.value as T | undefined; } function requestAsPromise(req: IDBRequest): Promise { - return new Promise((resolve, reject) => { - req.onsuccess = () => resolve(req.result as T); - req.onerror = () => reject(req.error); - }); + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req.result as T); + req.onerror = () => reject(req.error); + }); } diff --git a/website/src/website/switch/fs.ts b/website/src/website/switch/fs.ts index ccb9be0c..eac61137 100644 --- a/website/src/website/switch/fs.ts +++ b/website/src/website/switch/fs.ts @@ -1,113 +1,162 @@ import { put, get, type FsHandleRecord } from "./db"; import { nanoid } from "./uid"; -export async function pickDirectory(): Promise<{ id: string; handle: FileSystemDirectoryHandle }> { - // @ts-ignore - const handle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker(); - const id = nanoid(); - const rec: FsHandleRecord = { id, handle }; - await put("fsHandles", rec); - return { id, handle }; +export async function pickDirectory(): Promise<{ + id: string; + handle: FileSystemDirectoryHandle; +}> { + // @ts-ignore + const handle: FileSystemDirectoryHandle = await ( + window as any + ).showDirectoryPicker(); + const id = nanoid(); + const rec: FsHandleRecord = { id, handle }; + await put("fsHandles", rec); + return { id, handle }; } -export async function getDirectoryHandle(id: string): Promise { - const rec = await get("fsHandles", id); - return rec?.handle; +export async function getDirectoryHandle( + id: string +): Promise { + const rec = await get("fsHandles", id); + return rec?.handle; } -export async function ensureReadPerm(dir: FileSystemDirectoryHandle): Promise { - const perm = await (dir as any).queryPermission?.({ mode: "read" }); - if (perm === "granted") return true; - const req = await (dir as any).requestPermission?.({ mode: "read" }); - return req === "granted"; +export async function ensureReadPerm( + dir: FileSystemDirectoryHandle +): Promise { + const perm = await (dir as any).queryPermission?.({ mode: "read" }); + if (perm === "granted") return true; + const req = await (dir as any).requestPermission?.({ mode: "read" }); + return req === "granted"; } -export async function ensureWritePerm(dir: FileSystemDirectoryHandle): Promise { - const perm = await (dir as any).queryPermission?.({ mode: "readwrite" }); - if (perm === "granted") return true; - const req = await (dir as any).requestPermission?.({ mode: "readwrite" }); - return req === "granted"; +export async function ensureWritePerm( + dir: FileSystemDirectoryHandle +): Promise { + const perm = await (dir as any).queryPermission?.({ mode: "readwrite" }); + if (perm === "granted") return true; + const req = await (dir as any).requestPermission?.({ mode: "readwrite" }); + return req === "granted"; } -export async function getDirectoryHandleByPath(root: FileSystemDirectoryHandle, path: string, create = false): Promise { - const parts = path.split("/").filter(Boolean); - let cur: FileSystemDirectoryHandle = root; - for (const name of parts) { - const next = await (cur as any).getDirectoryHandle(name, { create }).catch(() => undefined); - if (!next) return undefined; - cur = next; - } - return cur; +export async function getDirectoryHandleByPath( + root: FileSystemDirectoryHandle, + path: string, + create = false +): Promise { + const parts = path.split("/").filter(Boolean); + let cur: FileSystemDirectoryHandle = root; + for (const name of parts) { + const next = await (cur as any) + .getDirectoryHandle(name, { create }) + .catch(() => undefined); + if (!next) return undefined; + cur = next; + } + return cur; } -export async function getFileHandleByPath(root: FileSystemDirectoryHandle, path: string, create = false): Promise { - const parts = path.split("/"); - const dirPath = parts.slice(0, -1).join("/"); - const fileName = parts[parts.length - 1]; - const dir = dirPath ? await getDirectoryHandleByPath(root, dirPath, create) : root; - if (!dir) return undefined; - return (dir as any).getFileHandle(fileName, { create }).catch(() => undefined); +export async function getFileHandleByPath( + root: FileSystemDirectoryHandle, + path: string, + create = false +): Promise { + const parts = path.split("/"); + const dirPath = parts.slice(0, -1).join("/"); + const fileName = parts[parts.length - 1]; + const dir = dirPath + ? await getDirectoryHandleByPath(root, dirPath, create) + : root; + if (!dir) return undefined; + return (dir as any) + .getFileHandle(fileName, { create }) + .catch(() => undefined); } -export async function readFileText(root: FileSystemDirectoryHandle, path: string): Promise { - const fh = await getFileHandleByPath(root, path); - if (!fh) return undefined; - const file = await fh.getFile(); - return file.text(); +export async function readFileText( + root: FileSystemDirectoryHandle, + path: string +): Promise { + const fh = await getFileHandleByPath(root, path); + if (!fh) return undefined; + const file = await fh.getFile(); + return file.text(); } -export async function writeFileText(root: FileSystemDirectoryHandle, path: string, content: string): Promise { - const dirPerm = await ensureWritePerm(root); - if (!dirPerm) throw new Error("No write permission"); - const fh = await getFileHandleByPath(root, path, true); - if (!fh) throw new Error("Cannot create file"); - const w = await (fh as any).createWritable(); - await w.write(content); - await w.close(); +export async function writeFileText( + root: FileSystemDirectoryHandle, + path: string, + content: string +): Promise { + const dirPerm = await ensureWritePerm(root); + if (!dirPerm) throw new Error("No write permission"); + const fh = await getFileHandleByPath(root, path, true); + if (!fh) throw new Error("Cannot create file"); + const w = await (fh as any).createWritable(); + await w.write(content); + await w.close(); } -export async function createDirectory(root: FileSystemDirectoryHandle, path: string): Promise { - const ok = await ensureWritePerm(root); - if (!ok) throw new Error("No write permission"); - await getDirectoryHandleByPath(root, path, true); +export async function createDirectory( + root: FileSystemDirectoryHandle, + path: string +): Promise { + const ok = await ensureWritePerm(root); + if (!ok) throw new Error("No write permission"); + await getDirectoryHandleByPath(root, path, true); } -export async function deleteEntry(root: FileSystemDirectoryHandle, path: string, recursive = false): Promise { - const parts = path.split("/"); - const dirPath = parts.slice(0, -1).join("/"); - const name = parts[parts.length - 1]; - const dir = dirPath ? await getDirectoryHandleByPath(root, dirPath) : root; - if (!dir) throw new Error("Path not found"); - await (dir as any).removeEntry(name, { recursive }).catch(() => undefined); +export async function deleteEntry( + root: FileSystemDirectoryHandle, + path: string, + recursive = false +): Promise { + const parts = path.split("/"); + const dirPath = parts.slice(0, -1).join("/"); + const name = parts[parts.length - 1]; + const dir = dirPath ? await getDirectoryHandleByPath(root, dirPath) : root; + if (!dir) throw new Error("Path not found"); + await (dir as any).removeEntry(name, { recursive }).catch(() => undefined); } -export async function* walk(dir: FileSystemDirectoryHandle, pathPrefix = ""): AsyncGenerator<{ path: string; file: File }>{ - // @ts-ignore - for await (const [name, entry] of (dir as any).entries()) { - const p = pathPrefix ? `${pathPrefix}/${name}` : name; - if (entry.kind === "directory") { - yield* walk(entry as FileSystemDirectoryHandle, p); - } else { - const file = await (entry as FileSystemFileHandle).getFile(); - yield { path: p, file }; - } - } +export async function* walk( + dir: FileSystemDirectoryHandle, + pathPrefix = "" +): AsyncGenerator<{ path: string; file: File }> { + // @ts-ignore + for await (const [name, entry] of (dir as any).entries()) { + const p = pathPrefix ? `${pathPrefix}/${name}` : name; + if (entry.kind === "directory") { + yield* walk(entry as FileSystemDirectoryHandle, p); + } else { + const file = await (entry as FileSystemFileHandle).getFile(); + yield { path: p, file }; + } + } } -export async function copyDirectory(src: FileSystemDirectoryHandle, dest: FileSystemDirectoryHandle): Promise { - const ok = await ensureWritePerm(dest); - if (!ok) throw new Error("No write permission on destination"); - // @ts-ignore - for await (const [name, entry] of (src as any).entries()) { - if (entry.kind === "directory") { - const sub = await (dest as any).getDirectoryHandle(name, { create: true }); - await copyDirectory(entry as FileSystemDirectoryHandle, sub); - } else { - const file = await (entry as FileSystemFileHandle).getFile(); - const fh = await (dest as any).getFileHandle(name, { create: true }); - const w = await (fh as any).createWritable(); - await w.write(await file.arrayBuffer()); - await w.close(); - } - } +export async function copyDirectory( + src: FileSystemDirectoryHandle, + dest: FileSystemDirectoryHandle +): Promise { + const ok = await ensureWritePerm(dest); + if (!ok) throw new Error("No write permission on destination"); + // @ts-ignore + for await (const [name, entry] of (src as any).entries()) { + if (entry.kind === "directory") { + const sub = await (dest as any).getDirectoryHandle(name, { + create: true, + }); + await copyDirectory(entry as FileSystemDirectoryHandle, sub); + } else { + const file = await (entry as FileSystemFileHandle).getFile(); + const fh = await (dest as any).getFileHandle(name, { + create: true, + }); + const w = await (fh as any).createWritable(); + await w.write(await file.arrayBuffer()); + await w.close(); + } + } } diff --git a/website/src/website/switch/supabase.ts b/website/src/website/switch/supabase.ts index 57c8b7d5..51e5464e 100644 --- a/website/src/website/switch/supabase.ts +++ b/website/src/website/switch/supabase.ts @@ -1,5 +1,6 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { createClient, SupabaseClient } from "@supabase/supabase-js"; const url = process.env.SUPABASE_URL as string; const key = process.env.SUPABASE_ANON_KEY as string; -export const supabase: SupabaseClient | undefined = url && key ? createClient(url, key) : undefined; +export const supabase: SupabaseClient | undefined = + url && key ? createClient(url, key) : undefined; diff --git a/website/src/website/switch/sync.ts b/website/src/website/switch/sync.ts index 4e2b648c..8cf326f9 100644 --- a/website/src/website/switch/sync.ts +++ b/website/src/website/switch/sync.ts @@ -1,37 +1,64 @@ -import { getAll, getAllByIndex, type RepoRecord, type BranchRecord, type CommitRecord, type IssueRecord } from './db'; -import { supabase } from './supabase'; +import { + getAll, + getAllByIndex, + type RepoRecord, + type BranchRecord, + type CommitRecord, + type IssueRecord, +} from "./db"; +import { supabase } from "./supabase"; export interface ExportBundle { - repo: RepoRecord; - branches: BranchRecord[]; - commits: CommitRecord[]; - issues: IssueRecord[]; - exportedAt: number; + repo: RepoRecord; + branches: BranchRecord[]; + commits: CommitRecord[]; + issues: IssueRecord[]; + exportedAt: number; } export async function exportRepoBundle(repoId: string): Promise { - const repos = await getAll('repos'); - const repo = repos.find(r=>r.id===repoId)!; - const branches = await getAllByIndex('branches','by_repo', repoId); - const commits = await getAllByIndex('commits','by_repo', repoId); - const issues = await getAllByIndex('issues','by_repo', repoId); - return { repo, branches, commits, issues, exportedAt: Date.now() }; + const repos = await getAll("repos"); + const repo = repos.find((r) => r.id === repoId)!; + const branches = await getAllByIndex( + "branches", + "by_repo", + repoId + ); + const commits = await getAllByIndex( + "commits", + "by_repo", + repoId + ); + const issues = await getAllByIndex( + "issues", + "by_repo", + repoId + ); + return { repo, branches, commits, issues, exportedAt: Date.now() }; } export async function pushRepoToSupabase(repoId: string): Promise { - if (!supabase) throw new Error('Supabase not configured'); - const bundle = await exportRepoBundle(repoId); - const path = `repos/${repoId}.json`; - const data = new Blob([JSON.stringify(bundle)], { type: 'application/json' }); - const { error } = await supabase.storage.from('switch').upload(path, data, { upsert: true }); - if (error) throw error; + if (!supabase) throw new Error("Supabase not configured"); + const bundle = await exportRepoBundle(repoId); + const path = `repos/${repoId}.json`; + const data = new Blob([JSON.stringify(bundle)], { + type: "application/json", + }); + const { error } = await supabase.storage + .from("switch") + .upload(path, data, { upsert: true }); + if (error) throw error; } -export async function pullRepoFromSupabase(repoId: string): Promise { - if (!supabase) throw new Error('Supabase not configured'); - const path = `repos/${repoId}.json`; - const { data, error } = await supabase.storage.from('switch').download(path); - if (error) throw error; - const text = await data.text(); - return JSON.parse(text) as ExportBundle; +export async function pullRepoFromSupabase( + repoId: string +): Promise { + if (!supabase) throw new Error("Supabase not configured"); + const path = `repos/${repoId}.json`; + const { data, error } = await supabase.storage + .from("switch") + .download(path); + if (error) throw error; + const text = await data.text(); + return JSON.parse(text) as ExportBundle; } diff --git a/website/src/website/switch/templates.ts b/website/src/website/switch/templates.ts index 2a885239..6601d89a 100644 --- a/website/src/website/switch/templates.ts +++ b/website/src/website/switch/templates.ts @@ -1,26 +1,41 @@ -import { get, put, del, getAll, type RepoTemplateRecord, type IssueTemplateRecord } from "./db"; +import { + get, + put, + del, + getAll, + type RepoTemplateRecord, + type IssueTemplateRecord, +} from "./db"; import { nanoid } from "./uid"; -export async function createRepoTemplate(name: string, fsHandleId: string): Promise { - const rec: RepoTemplateRecord = { id: nanoid(), name, fsHandleId }; - await put("repoTemplates" as any, rec as any); - return rec; +export async function createRepoTemplate( + name: string, + fsHandleId: string +): Promise { + const rec: RepoTemplateRecord = { id: nanoid(), name, fsHandleId }; + await put("repoTemplates" as any, rec as any); + return rec; } export async function listRepoTemplates(): Promise { - return getAll("repoTemplates" as any) as any; + return getAll("repoTemplates" as any) as any; } export async function deleteRepoTemplate(id: string): Promise { - await del("repoTemplates" as any, id); + await del("repoTemplates" as any, id); } -export async function createIssueTemplate(data: { name: string; title: string; body: string; labels: string[] }): Promise { - const rec: IssueTemplateRecord = { id: nanoid(), ...data }; - await put("issueTemplates" as any, rec as any); - return rec; +export async function createIssueTemplate(data: { + name: string; + title: string; + body: string; + labels: string[]; +}): Promise { + const rec: IssueTemplateRecord = { id: nanoid(), ...data }; + await put("issueTemplates" as any, rec as any); + return rec; } export async function listIssueTemplates(): Promise { - return getAll("issueTemplates" as any) as any; + return getAll("issueTemplates" as any) as any; } export async function deleteIssueTemplate(id: string): Promise { - await del("issueTemplates" as any, id); + await del("issueTemplates" as any, id); } diff --git a/website/webpack.config.ts b/website/webpack.config.ts index 0175dc2b..5affb991 100644 --- a/website/webpack.config.ts +++ b/website/webpack.config.ts @@ -76,7 +76,9 @@ module.exports = { "process.env": { YEAR: JSON.stringify(new Date().getFullYear()), SUPABASE_URL: JSON.stringify(process.env.SUPABASE_URL || ""), - SUPABASE_ANON_KEY: JSON.stringify(process.env.SUPABASE_ANON_KEY || ""), + SUPABASE_ANON_KEY: JSON.stringify( + process.env.SUPABASE_ANON_KEY || "" + ), }, }), new CleanWebpackPlugin(), @@ -119,7 +121,11 @@ module.exports = { }), new CopyPlugin({ patterns: [ - { from: "./typedoc/dist", to: "./typedoc/", noErrorOnMissing: true }, + { + from: "./typedoc/dist", + to: "./typedoc/", + noErrorOnMissing: true, + }, ], }), new CopyPlugin({