Prettier format pending files

This commit is contained in:
Builder.io 2025-09-18 05:10:58 +00:00
parent c3b3d72037
commit 34b0d92e8a
11 changed files with 1330 additions and 726 deletions

View file

@ -1,14 +1,23 @@
const { app, BrowserWindow } = require('electron'); const { app, BrowserWindow } = require("electron");
const path = require('path'); const path = require("path");
function createWindow() { function createWindow() {
const win = new BrowserWindow({ width: 1280, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true } }); const win = new BrowserWindow({
const startUrl = process.env.SWITCH_URL || 'http://localhost:8080/switch.html'; width: 1280,
height: 800,
webPreferences: { nodeIntegration: false, contextIsolation: true },
});
const startUrl =
process.env.SWITCH_URL || "http://localhost:8080/switch.html";
win.loadURL(startUrl); win.loadURL(startUrl);
} }
app.whenReady().then(() => { app.whenReady().then(() => {
createWindow(); createWindow();
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) 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(); });

View file

@ -1,26 +1,54 @@
const CACHE = 'switch-cache-v1'; const CACHE = "switch-cache-v1";
const ASSETS = [ const ASSETS = [
'/', "/",
'/index.html', "/index.html",
'/playground.html', "/playground.html",
'/monarch.html', "/monarch.html",
'/switch.html' "/switch.html",
]; ];
self.addEventListener('install', (e) => { self.addEventListener("install", (e) => {
e.waitUntil(caches.open(CACHE).then(c=>c.addAll(ASSETS)).then(()=>self.skipWaiting())); e.waitUntil(
caches
.open(CACHE)
.then((c) => c.addAll(ASSETS))
.then(() => self.skipWaiting())
);
}); });
self.addEventListener('activate', (e) => { 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())); 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) => { self.addEventListener("fetch", (e) => {
const url = new URL(e.request.url); const url = new URL(e.request.url);
if (url.origin === location.origin) { if (url.origin === location.origin) {
e.respondWith(caches.match(e.request).then(r=> r || fetch(e.request).then(resp=>{ e.respondWith(
if (e.request.method==='GET' && resp.ok && resp.type==='basic') { 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(); const clone = resp.clone();
caches.open(CACHE).then(c=>c.put(e.request, clone)); caches
.open(CACHE)
.then((c) => c.put(e.request, clone));
} }
return resp; return resp;
}).catch(()=>caches.match('/index.html')))); })
.catch(() => caches.match("/index.html"))
)
);
} }
}); });

View file

@ -93,7 +93,10 @@ async function _loadMonaco(setup: IMonacoSetup): Promise<typeof monaco> {
); );
} catch (e) { } catch (e) {
// If loading optional language contributions fails, still resolve the editor to keep app functional. // 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); res(monaco);
} }
}); });

View file

@ -9,8 +9,8 @@ elem.className = "root";
document.body.append(elem); document.body.append(elem);
ReactDOM.render(<App />, elem); ReactDOM.render(<App />, elem);
if ('serviceWorker' in navigator) { if ("serviceWorker" in navigator) {
window.addEventListener('load', () => { window.addEventListener("load", () => {
navigator.serviceWorker.register('/sw.js').catch(() => {}); navigator.serviceWorker.register("/sw.js").catch(() => {});
}); });
} }

View file

@ -1,42 +1,83 @@
import * as React from "react"; import * as React from "react";
import { ControlledMonacoEditor } from "../../components/monaco/MonacoEditor"; import { ControlledMonacoEditor } from "../../components/monaco/MonacoEditor";
import "../../switch.scss"; import "../../switch.scss";
import { createRepository, listRepositories, listBranches, createBranch } from "../../switch/models"; import {
import { pickDirectory, getDirectoryHandle, ensureReadPerm, walk } from "../../switch/fs"; createRepository,
import type { RepoRecord, BranchRecord, CommitRecord, IssueRecord } from "../../switch/db"; 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 { 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 } interface OpenFile {
path: string;
content: string;
language: string;
}
export function SwitchPage() { export function SwitchPage() {
const [repos, setRepos] = React.useState<RepoRecord[]>([]); const [repos, setRepos] = React.useState<RepoRecord[]>([]);
const [activeRepo, setActiveRepo] = React.useState<RepoRecord | undefined>(); const [activeRepo, setActiveRepo] = React.useState<
const [tree, setTree] = React.useState<{ path: string; isDir: boolean; children?: any[] }[]>([]); RepoRecord | undefined
>();
const [tree, setTree] = React.useState<
{ path: string; isDir: boolean; children?: any[] }[]
>([]);
const [openFile, setOpenFile] = React.useState<OpenFile | undefined>(); const [openFile, setOpenFile] = React.useState<OpenFile | undefined>();
const [editorValue, setEditorValue] = React.useState<string>("// SWITCH: Open a folder to get started\n"); const [editorValue, setEditorValue] = React.useState<string>(
"// SWITCH: Open a folder to get started\n"
);
const [branches, setBranches] = React.useState<BranchRecord[]>([]); const [branches, setBranches] = React.useState<BranchRecord[]>([]);
const [currentBranchId, setCurrentBranchIdState] = React.useState<string | undefined>(undefined); const [currentBranchId, setCurrentBranchIdState] = React.useState<
string | undefined
>(undefined);
const [commitMsg, setCommitMsg] = React.useState(""); const [commitMsg, setCommitMsg] = React.useState("");
const [history, setHistory] = React.useState<CommitRecord[]>([]); const [history, setHistory] = React.useState<CommitRecord[]>([]);
const [issues, setIssues] = React.useState<IssueRecord[]>([]); const [issues, setIssues] = React.useState<IssueRecord[]>([]);
const [newIssueTitle, setNewIssueTitle] = React.useState(""); const [newIssueTitle, setNewIssueTitle] = React.useState("");
const [selectedIssueId, setSelectedIssueId] = React.useState<string | undefined>(undefined); const [selectedIssueId, setSelectedIssueId] = React.useState<
const selectedIssue = issues.find(i=>i.id===selectedIssueId); string | undefined
>(undefined);
const selectedIssue = issues.find((i) => i.id === selectedIssueId);
const [issueTitle, setIssueTitle] = React.useState(""); const [issueTitle, setIssueTitle] = React.useState("");
const [issueBody, setIssueBody] = React.useState(""); const [issueBody, setIssueBody] = React.useState("");
const [issueLabels, setIssueLabels] = React.useState<string>(""); const [issueLabels, setIssueLabels] = React.useState<string>("");
const [issueStatus, setIssueStatus] = React.useState<"open"|"closed">("open"); const [issueStatus, setIssueStatus] = React.useState<"open" | "closed">(
"open"
);
React.useEffect(() => { React.useEffect(() => {
refreshRepos(); refreshRepos();
}, []); }, []);
React.useEffect(() => { (async () => { React.useEffect(() => {
(async () => {
if (!activeRepo) return; if (!activeRepo) return;
const b = await listBranches(activeRepo.id); const b = await listBranches(activeRepo.id);
setBranches(b); setBranches(b);
const cur = await getCurrentBranchId(activeRepo.id) || b.find(x=>x.name===activeRepo.defaultBranch)?.id; const cur =
(await getCurrentBranchId(activeRepo.id)) ||
b.find((x) => x.name === activeRepo.defaultBranch)?.id;
if (cur) { if (cur) {
setCurrentBranchIdState(cur); setCurrentBranchIdState(cur);
await refreshHistory(activeRepo.id, cur); await refreshHistory(activeRepo.id, cur);
@ -44,11 +85,12 @@ export function SwitchPage() {
const iss = await listIssues(activeRepo.id); const iss = await listIssues(activeRepo.id);
setIssues(iss); setIssues(iss);
if (iss.length) selectIssue(iss[0].id); if (iss.length) selectIssue(iss[0].id);
})(); }, [activeRepo?.id]); })();
}, [activeRepo?.id]);
function selectIssue(id: string) { function selectIssue(id: string) {
setSelectedIssueId(id); setSelectedIssueId(id);
const it = issues.find(i=>i.id===id); const it = issues.find((i) => i.id === id);
if (it) { if (it) {
setIssueTitle(it.title); setIssueTitle(it.title);
setIssueBody(it.body); setIssueBody(it.body);
@ -88,7 +130,12 @@ export function SwitchPage() {
const key = parts.slice(0, i + 1).join("/"); const key = parts.slice(0, i + 1).join("/");
const isDir = i < parts.length - 1; const isDir = i < parts.length - 1;
cur.children = cur.children || new Map<string, any>(); cur.children = cur.children || new Map<string, any>();
if (!cur.children.has(key)) cur.children.set(key, { path: key, isDir, children: undefined }); if (!cur.children.has(key))
cur.children.set(key, {
path: key,
isDir,
children: undefined,
});
cur = cur.children.get(key); cur = cur.children.get(key);
} }
} }
@ -96,7 +143,13 @@ export function SwitchPage() {
if (!node.children) return []; if (!node.children) return [];
const arr = Array.from(node.children.values()); const arr = Array.from(node.children.values());
for (const n of arr) if (n.isDir) n.children = toArray(n); 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)); arr.sort((a, b) =>
a.isDir === b.isDir
? a.path.localeCompare(b.path)
: a.isDir
? -1
: 1
);
return arr; return arr;
}; };
setTree(toArray(root)); setTree(toArray(root));
@ -121,7 +174,7 @@ export function SwitchPage() {
} }
async function refreshHistory(repoId: string, branchId: string) { async function refreshHistory(repoId: string, branchId: string) {
const b = branches.find(x=>x.id===branchId); const b = branches.find((x) => x.id === branchId);
const list = await listCommitsReachable(repoId, b?.headCommitId); const list = await listCommitsReachable(repoId, b?.headCommitId);
setHistory(list); setHistory(list);
} }
@ -129,22 +182,61 @@ export function SwitchPage() {
return ( return (
<div className="switch-shell"> <div className="switch-shell">
<div className="switch-activity-bar" aria-label="Activity Bar"> <div className="switch-activity-bar" aria-label="Activity Bar">
<button className="switch-activity-item" title="Explorer" aria-label="Explorer">📁</button> <button
<button className="switch-activity-item" title="Search" aria-label="Search">🔎</button> className="switch-activity-item"
<button className="switch-activity-item" title="Source Control" aria-label="Source Control">🔀</button> title="Explorer"
<button className="switch-activity-item" title="Issues" aria-label="Issues">🏷</button> aria-label="Explorer"
>
📁
</button>
<button
className="switch-activity-item"
title="Search"
aria-label="Search"
>
🔎
</button>
<button
className="switch-activity-item"
title="Source Control"
aria-label="Source Control"
>
🔀
</button>
<button
className="switch-activity-item"
title="Issues"
aria-label="Issues"
>
🏷
</button>
</div> </div>
<div className="switch-main"> <div className="switch-main">
<aside className="switch-sidebar" aria-label="Sidebar"> <aside className="switch-sidebar" aria-label="Sidebar">
<div className="switch-pane"> <div className="switch-pane">
<div className="switch-pane-header">Repositories</div> <div className="switch-pane-header">Repositories</div>
<div className="switch-pane-body"> <div className="switch-pane-body">
<button className="switch-secondary-btn" onClick={onOpenFolder}>Open Folder</button> <button
className="switch-secondary-btn"
onClick={onOpenFolder}
>
Open Folder
</button>
<div> <div>
{repos.map((r) => ( {repos.map((r) => (
<div key={r.id}> <div key={r.id}>
<label> <label>
<input type="radio" name="repo" onChange={() => onSelectRepo(r.id)} checked={activeRepo?.id === r.id} /> {r.name} <input
type="radio"
name="repo"
onChange={() =>
onSelectRepo(r.id)
}
checked={
activeRepo?.id === r.id
}
/>{" "}
{r.name}
</label> </label>
</div> </div>
))} ))}
@ -155,21 +247,116 @@ export function SwitchPage() {
<div className="switch-pane-header">Branches</div> <div className="switch-pane-header">Branches</div>
<div className="switch-pane-body"> <div className="switch-pane-body">
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<select value={currentBranchId || ""} onChange={async (e)=>{ if (!activeRepo) return; const id=e.target.value; setCurrentBranchIdState(id); await setCurrentBranchId(activeRepo.id, id); await refreshHistory(activeRepo.id, id); }}> <select
<option value="" disabled> Select branch </option> value={currentBranchId || ""}
{branches.map(b=> (<option key={b.id} value={b.id}>{b.name}</option>))} onChange={async (e) => {
if (!activeRepo) return;
const id = e.target.value;
setCurrentBranchIdState(id);
await setCurrentBranchId(
activeRepo.id,
id
);
await refreshHistory(activeRepo.id, id);
}}
>
<option value="" disabled>
Select branch
</option>
{branches.map((b) => (
<option key={b.id} value={b.id}>
{b.name}
</option>
))}
</select> </select>
<button className="switch-secondary-btn" onClick={async ()=>{ if (!activeRepo) return; const name=prompt("New branch name:", "feature"); if (!name) return; const from = branches.find(b=>b.id===currentBranchId)?.headCommitId; const b = await createBranch(activeRepo.id, name, from); const list=await listBranches(activeRepo.id); setBranches(list); setCurrentBranchIdState(b.id); await setCurrentBranchId(activeRepo.id, b.id); await refreshHistory(activeRepo.id, b.id); }}>New Branch</button> <button
className="switch-secondary-btn"
onClick={async () => {
if (!activeRepo) return;
const name = prompt(
"New branch name:",
"feature"
);
if (!name) return;
const from = branches.find(
(b) => b.id === currentBranchId
)?.headCommitId;
const b = await createBranch(
activeRepo.id,
name,
from
);
const list = await listBranches(
activeRepo.id
);
setBranches(list);
setCurrentBranchIdState(b.id);
await setCurrentBranchId(
activeRepo.id,
b.id
);
await refreshHistory(
activeRepo.id,
b.id
);
}}
>
New Branch
</button>
</div> </div>
<div> <div>
<input placeholder="Commit message" value={commitMsg} onChange={(e)=>setCommitMsg(e.target.value)} /> <input
<button className="switch-secondary-btn" onClick={async ()=>{ if (!activeRepo||!currentBranchId) return; await commitOnBranch(activeRepo.id, currentBranchId, commitMsg || "chore: update"); setCommitMsg(""); await refreshHistory(activeRepo.id, currentBranchId); }}>Commit</button> placeholder="Commit message"
value={commitMsg}
onChange={(e) =>
setCommitMsg(e.target.value)
}
/>
<button
className="switch-secondary-btn"
onClick={async () => {
if (!activeRepo || !currentBranchId)
return;
await commitOnBranch(
activeRepo.id,
currentBranchId,
commitMsg || "chore: update"
);
setCommitMsg("");
await refreshHistory(
activeRepo.id,
currentBranchId
);
}}
>
Commit
</button>
</div> </div>
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<div className="switch-pane-header">History</div> <div className="switch-pane-header">
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}> History
{history.map(c => (<li key={c.id}> {new Date(c.timestamp).toLocaleString()} {c.message}</li>))} </div>
{history.length===0 && <li className="switch-empty">No commits</li>} <ul
style={{
listStyle: "none",
padding: 0,
margin: 0,
}}
>
{history.map((c) => (
<li key={c.id}>
{" "}
{new Date(
c.timestamp
).toLocaleString()}{" "}
{c.message}
</li>
))}
{history.length === 0 && (
<li className="switch-empty">
No commits
</li>
)}
</ul> </ul>
</div> </div>
</div> </div>
@ -178,13 +365,43 @@ export function SwitchPage() {
<div className="switch-pane-header">Templates</div> <div className="switch-pane-header">Templates</div>
<div className="switch-pane-body"> <div className="switch-pane-body">
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<button className="switch-secondary-btn" onClick={onSaveRepoAsTemplate} disabled={!activeRepo}>Save Current Repo as Template</button> <button
<button className="switch-secondary-btn" onClick={onCreateRepoFromTemplate}>New Repo from Template</button> className="switch-secondary-btn"
onClick={onSaveRepoAsTemplate}
disabled={!activeRepo}
>
Save Current Repo as Template
</button>
<button
className="switch-secondary-btn"
onClick={onCreateRepoFromTemplate}
>
New Repo from Template
</button>
</div> </div>
<div> <div>
<div style={{ fontSize: 12, color: '#9da5b4', margin: '6px 0' }}>Issue Templates</div> <div
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}> style={{
<button className="switch-secondary-btn" onClick={onCreateIssueTemplate}>New Issue Template</button> fontSize: 12,
color: "#9da5b4",
margin: "6px 0",
}}
>
Issue Templates
</div>
<div
style={{
display: "flex",
gap: 6,
marginBottom: 6,
}}
>
<button
className="switch-secondary-btn"
onClick={onCreateIssueTemplate}
>
New Issue Template
</button>
</div> </div>
</div> </div>
</div> </div>
@ -192,12 +409,26 @@ export function SwitchPage() {
<div className="switch-pane"> <div className="switch-pane">
<div className="switch-pane-header">Cloud Sync</div> <div className="switch-pane-header">Cloud Sync</div>
<div className="switch-pane-body"> <div className="switch-pane-body">
<div style={{ marginBottom: 8, color: '#9da5b4' }}> <div style={{ marginBottom: 8, color: "#9da5b4" }}>
Supabase storage bucket "switch" is required (public read/write via RLS). URL and anon key are read from env. Supabase storage bucket "switch" is required
(public read/write via RLS). URL and anon key
are read from env.
</div> </div>
<div> <div>
<button className="switch-secondary-btn" onClick={onPushToCloud} disabled={!activeRepo}>Push</button> <button
<button className="switch-secondary-btn" onClick={onPullFromCloud} disabled={!activeRepo}>Pull</button> className="switch-secondary-btn"
onClick={onPushToCloud}
disabled={!activeRepo}
>
Push
</button>
<button
className="switch-secondary-btn"
onClick={onPullFromCloud}
disabled={!activeRepo}
>
Pull
</button>
</div> </div>
</div> </div>
</div> </div>
@ -205,9 +436,24 @@ export function SwitchPage() {
<div className="switch-pane-header">Explorer</div> <div className="switch-pane-header">Explorer</div>
<div className="switch-pane-body"> <div className="switch-pane-body">
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<button className="switch-secondary-btn" onClick={onNewFile}>New File</button> <button
<button className="switch-secondary-btn" onClick={onNewFolder}>New Folder</button> className="switch-secondary-btn"
<button className="switch-secondary-btn" onClick={onRefresh}>Refresh</button> onClick={onNewFile}
>
New File
</button>
<button
className="switch-secondary-btn"
onClick={onNewFolder}
>
New Folder
</button>
<button
className="switch-secondary-btn"
onClick={onRefresh}
>
Refresh
</button>
</div> </div>
{tree.length === 0 ? ( {tree.length === 0 ? (
<div className="switch-empty">No files</div> <div className="switch-empty">No files</div>
@ -220,33 +466,128 @@ export function SwitchPage() {
<div className="switch-pane-header">Issues</div> <div className="switch-pane-header">Issues</div>
<div className="switch-pane-body"> <div className="switch-pane-body">
<div style={{ display: "flex", gap: 8 }}> <div style={{ display: "flex", gap: 8 }}>
<input placeholder="New issue title" value={newIssueTitle} onChange={(e)=>setNewIssueTitle(e.target.value)} /> <input
<button className="switch-secondary-btn" onClick={async ()=>{ if(!activeRepo||!newIssueTitle) return; await createIssue(activeRepo.id, { title: newIssueTitle, branchId: currentBranchId, filePath: openFile?.path }); setNewIssueTitle(""); setIssues(await listIssues(activeRepo.id)); }}>Add</button> placeholder="New issue title"
value={newIssueTitle}
onChange={(e) =>
setNewIssueTitle(e.target.value)
}
/>
<button
className="switch-secondary-btn"
onClick={async () => {
if (!activeRepo || !newIssueTitle)
return;
await createIssue(activeRepo.id, {
title: newIssueTitle,
branchId: currentBranchId,
filePath: openFile?.path,
});
setNewIssueTitle("");
setIssues(
await listIssues(activeRepo.id)
);
}}
>
Add
</button>
</div> </div>
<ul style={{ listStyle: "none", padding: 0, marginTop: 8 }}> <ul
{issues.map(i => ( style={{
<li key={i.id} onClick={()=>selectIssue(i.id)} style={{ cursor: "pointer", padding: "2px 0", color: selectedIssueId===i.id? "#fff": undefined }}> listStyle: "none",
#{i.id.slice(-5)} {i.title} <span style={{ color: "#858585" }}>({i.status})</span> padding: 0,
marginTop: 8,
}}
>
{issues.map((i) => (
<li
key={i.id}
onClick={() => selectIssue(i.id)}
style={{
cursor: "pointer",
padding: "2px 0",
color:
selectedIssueId === i.id
? "#fff"
: undefined,
}}
>
#{i.id.slice(-5)} {i.title}{" "}
<span style={{ color: "#858585" }}>
({i.status})
</span>
</li> </li>
))} ))}
{issues.length===0 && <li className="switch-empty">No issues</li>} {issues.length === 0 && (
<li className="switch-empty">No issues</li>
)}
</ul> </ul>
{selectedIssue && ( {selectedIssue && (
<div style={{ borderTop: "1px solid #2d2d2d", marginTop: 8, paddingTop: 8 }}> <div
<div style={{ marginBottom: 6 }}>Edit Issue</div> style={{
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}> borderTop: "1px solid #2d2d2d",
<input placeholder="Title" value={issueTitle} onChange={e=>setIssueTitle(e.target.value)} /> marginTop: 8,
<select value={issueStatus} onChange={(e)=>setIssueStatus(e.target.value as any)}> paddingTop: 8,
}}
>
<div style={{ marginBottom: 6 }}>
Edit Issue
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 6,
}}
>
<input
placeholder="Title"
value={issueTitle}
onChange={(e) =>
setIssueTitle(e.target.value)
}
/>
<select
value={issueStatus}
onChange={(e) =>
setIssueStatus(
e.target.value as any
)
}
>
<option value="open">open</option> <option value="open">open</option>
<option value="closed">closed</option> <option value="closed">
closed
</option>
</select> </select>
<input placeholder="labels (comma separated)" value={issueLabels} onChange={e=>setIssueLabels(e.target.value)} /> <input
placeholder="labels (comma separated)"
value={issueLabels}
onChange={(e) =>
setIssueLabels(e.target.value)
}
/>
<div style={{ height: 160 }}> <div style={{ height: 160 }}>
<ControlledMonacoEditor value={issueBody} onDidValueChange={setIssueBody} language="markdown" theme="vs-dark" /> <ControlledMonacoEditor
value={issueBody}
onDidValueChange={setIssueBody}
language="markdown"
theme="vs-dark"
/>
</div> </div>
<div> <div>
<button className="switch-secondary-btn" onClick={onSaveIssue}>Save</button> <button
<button className="switch-secondary-btn" onClick={onDeleteIssue}>Delete</button> className="switch-secondary-btn"
onClick={onSaveIssue}
>
Save
</button>
<button
className="switch-secondary-btn"
onClick={onDeleteIssue}
>
Delete
</button>
</div> </div>
</div> </div>
</div> </div>
@ -256,22 +597,55 @@ export function SwitchPage() {
</aside> </aside>
<section className="switch-editor" aria-label="Editor"> <section className="switch-editor" aria-label="Editor">
<div className="switch-tabbar"> <div className="switch-tabbar">
<div className="switch-tab active">{openFile?.path || "welcome.txt"}</div> <div className="switch-tab active">
{openFile?.path || "welcome.txt"}
</div>
<div style={{ marginLeft: "auto" }}> <div style={{ marginLeft: "auto" }}>
<button className="switch-secondary-btn" onClick={onSave} disabled={!openFile}>Save</button> <button
<button className="switch-secondary-btn" onClick={onSaveAs} disabled={!activeRepo}>Save As</button> className="switch-secondary-btn"
<button className="switch-secondary-btn" onClick={onRename} disabled={!openFile}>Rename</button> onClick={onSave}
<button className="switch-secondary-btn" onClick={onDelete} disabled={!openFile}>Delete</button> disabled={!openFile}
>
Save
</button>
<button
className="switch-secondary-btn"
onClick={onSaveAs}
disabled={!activeRepo}
>
Save As
</button>
<button
className="switch-secondary-btn"
onClick={onRename}
disabled={!openFile}
>
Rename
</button>
<button
className="switch-secondary-btn"
onClick={onDelete}
disabled={!openFile}
>
Delete
</button>
</div> </div>
</div> </div>
<div className="switch-editor-inner"> <div className="switch-editor-inner">
<ControlledMonacoEditor value={editorValue} onDidValueChange={setEditorValue} language={openFile?.language || "plaintext"} theme="vs-dark" /> <ControlledMonacoEditor
value={editorValue}
onDidValueChange={setEditorValue}
language={openFile?.language || "plaintext"}
theme="vs-dark"
/>
</div> </div>
</section> </section>
</div> </div>
<footer className="switch-statusbar" aria-label="Status Bar"> <footer className="switch-statusbar" aria-label="Status Bar">
<div className="switch-status-item">SWITCH</div> <div className="switch-status-item">SWITCH</div>
<div className="switch-status-item">{openFile?.language || "Plain Text"}</div> <div className="switch-status-item">
{openFile?.language || "Plain Text"}
</div>
<div className="switch-status-item">UTF-8</div> <div className="switch-status-item">UTF-8</div>
<div className="switch-status-item">LF</div> <div className="switch-status-item">LF</div>
</footer> </footer>
@ -305,17 +679,28 @@ export function SwitchPage() {
if (!activeRepo?.fsHandleId || !openFile) return; if (!activeRepo?.fsHandleId || !openFile) return;
const dir = await getDirectoryHandle(activeRepo.fsHandleId); const dir = await getDirectoryHandle(activeRepo.fsHandleId);
if (!dir) return; if (!dir) return;
await (await import("../../switch/fs")).writeFileText(dir, openFile.path, editorValue); await (
await import("../../switch/fs")
).writeFileText(dir, openFile.path, editorValue);
} }
async function onSaveAs() { async function onSaveAs() {
if (!activeRepo?.fsHandleId) return; if (!activeRepo?.fsHandleId) return;
const target = prompt("Save As path (relative):", openFile?.path || "untitled.txt"); const target = prompt(
"Save As path (relative):",
openFile?.path || "untitled.txt"
);
if (!target) return; if (!target) return;
const dir = await getDirectoryHandle(activeRepo.fsHandleId); const dir = await getDirectoryHandle(activeRepo.fsHandleId);
if (!dir) return; if (!dir) return;
await (await import("../../switch/fs")).writeFileText(dir, target, editorValue); await (
setOpenFile({ path: target, content: editorValue, language: languageFromPath(target) }); await import("../../switch/fs")
).writeFileText(dir, target, editorValue);
setOpenFile({
path: target,
content: editorValue,
language: languageFromPath(target),
});
await onRefresh(); await onRefresh();
} }
@ -331,16 +716,29 @@ export function SwitchPage() {
async function onCreateRepoFromTemplate() { async function onCreateRepoFromTemplate() {
const { listRepoTemplates } = await import("../../switch/templates"); const { listRepoTemplates } = await import("../../switch/templates");
const list = await listRepoTemplates(); const list = await listRepoTemplates();
if (!list.length) { alert("No repo templates."); return; } if (!list.length) {
alert("No repo templates.");
return;
}
const names = list.map((t, i) => `${i + 1}. ${t.name}`).join("\n"); const names = list.map((t, i) => `${i + 1}. ${t.name}`).join("\n");
const idxStr = prompt(`Choose template:\n${names}`, "1"); 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; if (!idxStr) return;
const idx = parseInt(idxStr, 10) - 1;
if (isNaN(idx) || idx < 0 || idx >= list.length) return;
const tpl = list[idx]; const tpl = list[idx];
const dest = await pickDirectory(); const dest = await pickDirectory();
const srcHandle = await getDirectoryHandle(tpl.fsHandleId); const srcHandle = await getDirectoryHandle(tpl.fsHandleId);
if (!srcHandle) { alert("Template folder permission required. Re-save the template."); return; } if (!srcHandle) {
await (await import("../../switch/fs")).copyDirectory(srcHandle, dest.handle); alert("Template folder permission required. Re-save the template.");
const repo = await createRepository(dest.handle.name || "workspace", dest.id); return;
}
await (
await import("../../switch/fs")
).copyDirectory(srcHandle, dest.handle);
const repo = await createRepository(
dest.handle.name || "workspace",
dest.id
);
await refreshRepos(); await refreshRepos();
setActiveRepo(repo); setActiveRepo(repo);
await loadTree(repo); await loadTree(repo);
@ -355,7 +753,11 @@ export function SwitchPage() {
const fs = await import("../../switch/fs"); const fs = await import("../../switch/fs");
await fs.writeFileText(dir, next, editorValue); await fs.writeFileText(dir, next, editorValue);
await fs.deleteEntry(dir, openFile.path); await fs.deleteEntry(dir, openFile.path);
setOpenFile({ path: next, content: editorValue, language: languageFromPath(next) }); setOpenFile({
path: next,
content: editorValue,
language: languageFromPath(next),
});
await onRefresh(); await onRefresh();
} }
@ -375,7 +777,10 @@ export function SwitchPage() {
const { updateIssue } = await import("../../switch/logic"); const { updateIssue } = await import("../../switch/logic");
selectedIssue.title = issueTitle; selectedIssue.title = issueTitle;
selectedIssue.body = issueBody; selectedIssue.body = issueBody;
selectedIssue.labels = issueLabels.split(",").map(s=>s.trim()).filter(Boolean); selectedIssue.labels = issueLabels
.split(",")
.map((s) => s.trim())
.filter(Boolean);
selectedIssue.status = issueStatus; selectedIssue.status = issueStatus;
await updateIssue(selectedIssue); await updateIssue(selectedIssue);
setIssues(await listIssues(activeRepo.id)); setIssues(await listIssues(activeRepo.id));
@ -405,25 +810,43 @@ export function SwitchPage() {
try { try {
const { pullRepoFromSupabase } = await import("../../switch/sync"); const { pullRepoFromSupabase } = await import("../../switch/sync");
const bundle = await pullRepoFromSupabase(activeRepo.id); const bundle = await pullRepoFromSupabase(activeRepo.id);
if (!bundle) { alert("No bundle found."); return; } if (!bundle) {
alert("No bundle found.");
return;
}
// For now, just show summary; full import/merge logic can be added as next step // 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}`); alert(
`Fetched bundle exportedAt=${new Date(
bundle.exportedAt
).toLocaleString()}\nbranches=${
bundle.branches.length
}, commits=${bundle.commits.length}, issues=${
bundle.issues.length
}`
);
} catch (e: any) { } catch (e: any) {
alert("Cloud pull failed: " + (e?.message || e)); alert("Cloud pull failed: " + (e?.message || e));
} }
} }
} }
async function getFileByPath(dir: FileSystemDirectoryHandle, path: string): Promise<File | undefined> { async function getFileByPath(
dir: FileSystemDirectoryHandle,
path: string
): Promise<File | undefined> {
const parts = path.split("/"); const parts = path.split("/");
let cur: FileSystemDirectoryHandle = dir; let cur: FileSystemDirectoryHandle = dir;
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
const name = parts[i]; const name = parts[i];
if (i === parts.length - 1) { if (i === parts.length - 1) {
const fh = await (cur as any).getFileHandle(name).catch(() => undefined) as FileSystemFileHandle | undefined; const fh = (await (cur as any)
.getFileHandle(name)
.catch(() => undefined)) as FileSystemFileHandle | undefined;
return fh ? fh.getFile() : undefined; return fh ? fh.getFile() : undefined;
} else { } else {
cur = await (cur as any).getDirectoryHandle(name).catch(() => undefined); cur = await (cur as any)
.getDirectoryHandle(name)
.catch(() => undefined);
if (!cur) return undefined as any; if (!cur) return undefined as any;
} }
} }
@ -433,26 +856,46 @@ async function getFileByPath(dir: FileSystemDirectoryHandle, path: string): Prom
function languageFromPath(path: string): string { function languageFromPath(path: string): string {
const ext = path.split(".").pop()?.toLowerCase(); const ext = path.split(".").pop()?.toLowerCase();
switch (ext) { switch (ext) {
case "ts": return "typescript"; case "ts":
case "tsx": return "typescript"; return "typescript";
case "js": return "javascript"; case "tsx":
case "jsx": return "javascript"; return "typescript";
case "json": return "json"; case "js":
case "css": return "css"; return "javascript";
case "scss": return "scss"; case "jsx":
case "html": return "html"; return "javascript";
case "md": return "markdown"; case "json":
case "py": return "python"; return "json";
case "rb": return "ruby"; case "css":
case "go": return "go"; return "css";
case "rs": return "rust"; case "scss":
case "java": return "java"; return "scss";
case "cs": return "csharp"; case "html":
case "cpp": return "cpp"; 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 "yml":
case "yaml": return "yaml"; case "yaml":
case "xml": return "xml"; return "yaml";
case "sql": return "sql"; case "xml":
default: return "plaintext"; return "xml";
case "sql":
return "sql";
default:
return "plaintext";
} }
} }

View file

@ -1,4 +1,10 @@
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_NAME = "switch-db";
const DB_VERSION = 2; const DB_VERSION = 2;
@ -68,12 +74,18 @@ export async function openDB(): Promise<IDBDatabase> {
store.createIndex("by_name", "name", { unique: false }); store.createIndex("by_name", "name", { unique: false });
} }
if (!db.objectStoreNames.contains("branches")) { if (!db.objectStoreNames.contains("branches")) {
const store = db.createObjectStore("branches", { keyPath: "id" }); const store = db.createObjectStore("branches", {
keyPath: "id",
});
store.createIndex("by_repo", "repoId", { unique: false }); store.createIndex("by_repo", "repoId", { unique: false });
store.createIndex("by_repo_name", ["repoId", "name"], { unique: true }); store.createIndex("by_repo_name", ["repoId", "name"], {
unique: true,
});
} }
if (!db.objectStoreNames.contains("commits")) { if (!db.objectStoreNames.contains("commits")) {
const store = db.createObjectStore("commits", { keyPath: "id" }); const store = db.createObjectStore("commits", {
keyPath: "id",
});
store.createIndex("by_repo", "repoId", { unique: false }); store.createIndex("by_repo", "repoId", { unique: false });
} }
if (!db.objectStoreNames.contains("issues")) { if (!db.objectStoreNames.contains("issues")) {
@ -98,7 +110,11 @@ export async function openDB(): Promise<IDBDatabase> {
}); });
} }
export async function tx<T>(storeNames: StoreName[], mode: IDBTransactionMode, fn: (tx: IDBTransaction) => Promise<T>): Promise<T> { export async function tx<T>(
storeNames: StoreName[],
mode: IDBTransactionMode,
fn: (tx: IDBTransaction) => Promise<T>
): Promise<T> {
const db = await openDB(); const db = await openDB();
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const transaction = db.transaction(storeNames, mode); const transaction = db.transaction(storeNames, mode);
@ -124,7 +140,10 @@ export async function put<T>(store: StoreName, value: T): Promise<void> {
}); });
} }
export async function get<T>(store: StoreName, key: IDBValidKey): Promise<T | undefined> { export async function get<T>(
store: StoreName,
key: IDBValidKey
): Promise<T | undefined> {
return tx([store], "readonly", async (t) => { return tx([store], "readonly", async (t) => {
return requestAsPromise<T | undefined>(t.objectStore(store).get(key)); return requestAsPromise<T | undefined>(t.objectStore(store).get(key));
}); });
@ -137,7 +156,11 @@ export async function del(store: StoreName, key: IDBValidKey): Promise<void> {
}); });
} }
export async function getAllByIndex<T>(store: StoreName, index: string, query: IDBValidKey | IDBKeyRange): Promise<T[]> { export async function getAllByIndex<T>(
store: StoreName,
index: string,
query: IDBValidKey | IDBKeyRange
): Promise<T[]> {
return tx([store], "readonly", async (t) => { return tx([store], "readonly", async (t) => {
const idx = t.objectStore(store).index(index); const idx = t.objectStore(store).index(index);
return requestAsPromise<T[]>(idx.getAll(query)); return requestAsPromise<T[]>(idx.getAll(query));

View file

@ -1,62 +1,94 @@
import { put, get, type FsHandleRecord } from "./db"; import { put, get, type FsHandleRecord } from "./db";
import { nanoid } from "./uid"; import { nanoid } from "./uid";
export async function pickDirectory(): Promise<{ id: string; handle: FileSystemDirectoryHandle }> { export async function pickDirectory(): Promise<{
id: string;
handle: FileSystemDirectoryHandle;
}> {
// @ts-ignore // @ts-ignore
const handle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker(); const handle: FileSystemDirectoryHandle = await (
window as any
).showDirectoryPicker();
const id = nanoid(); const id = nanoid();
const rec: FsHandleRecord = { id, handle }; const rec: FsHandleRecord = { id, handle };
await put("fsHandles", rec); await put("fsHandles", rec);
return { id, handle }; return { id, handle };
} }
export async function getDirectoryHandle(id: string): Promise<FileSystemDirectoryHandle | undefined> { export async function getDirectoryHandle(
id: string
): Promise<FileSystemDirectoryHandle | undefined> {
const rec = await get<FsHandleRecord>("fsHandles", id); const rec = await get<FsHandleRecord>("fsHandles", id);
return rec?.handle; return rec?.handle;
} }
export async function ensureReadPerm(dir: FileSystemDirectoryHandle): Promise<boolean> { export async function ensureReadPerm(
dir: FileSystemDirectoryHandle
): Promise<boolean> {
const perm = await (dir as any).queryPermission?.({ mode: "read" }); const perm = await (dir as any).queryPermission?.({ mode: "read" });
if (perm === "granted") return true; if (perm === "granted") return true;
const req = await (dir as any).requestPermission?.({ mode: "read" }); const req = await (dir as any).requestPermission?.({ mode: "read" });
return req === "granted"; return req === "granted";
} }
export async function ensureWritePerm(dir: FileSystemDirectoryHandle): Promise<boolean> { export async function ensureWritePerm(
dir: FileSystemDirectoryHandle
): Promise<boolean> {
const perm = await (dir as any).queryPermission?.({ mode: "readwrite" }); const perm = await (dir as any).queryPermission?.({ mode: "readwrite" });
if (perm === "granted") return true; if (perm === "granted") return true;
const req = await (dir as any).requestPermission?.({ mode: "readwrite" }); const req = await (dir as any).requestPermission?.({ mode: "readwrite" });
return req === "granted"; return req === "granted";
} }
export async function getDirectoryHandleByPath(root: FileSystemDirectoryHandle, path: string, create = false): Promise<FileSystemDirectoryHandle | undefined> { export async function getDirectoryHandleByPath(
root: FileSystemDirectoryHandle,
path: string,
create = false
): Promise<FileSystemDirectoryHandle | undefined> {
const parts = path.split("/").filter(Boolean); const parts = path.split("/").filter(Boolean);
let cur: FileSystemDirectoryHandle = root; let cur: FileSystemDirectoryHandle = root;
for (const name of parts) { for (const name of parts) {
const next = await (cur as any).getDirectoryHandle(name, { create }).catch(() => undefined); const next = await (cur as any)
.getDirectoryHandle(name, { create })
.catch(() => undefined);
if (!next) return undefined; if (!next) return undefined;
cur = next; cur = next;
} }
return cur; return cur;
} }
export async function getFileHandleByPath(root: FileSystemDirectoryHandle, path: string, create = false): Promise<FileSystemFileHandle | undefined> { export async function getFileHandleByPath(
root: FileSystemDirectoryHandle,
path: string,
create = false
): Promise<FileSystemFileHandle | undefined> {
const parts = path.split("/"); const parts = path.split("/");
const dirPath = parts.slice(0, -1).join("/"); const dirPath = parts.slice(0, -1).join("/");
const fileName = parts[parts.length - 1]; const fileName = parts[parts.length - 1];
const dir = dirPath ? await getDirectoryHandleByPath(root, dirPath, create) : root; const dir = dirPath
? await getDirectoryHandleByPath(root, dirPath, create)
: root;
if (!dir) return undefined; if (!dir) return undefined;
return (dir as any).getFileHandle(fileName, { create }).catch(() => undefined); return (dir as any)
.getFileHandle(fileName, { create })
.catch(() => undefined);
} }
export async function readFileText(root: FileSystemDirectoryHandle, path: string): Promise<string | undefined> { export async function readFileText(
root: FileSystemDirectoryHandle,
path: string
): Promise<string | undefined> {
const fh = await getFileHandleByPath(root, path); const fh = await getFileHandleByPath(root, path);
if (!fh) return undefined; if (!fh) return undefined;
const file = await fh.getFile(); const file = await fh.getFile();
return file.text(); return file.text();
} }
export async function writeFileText(root: FileSystemDirectoryHandle, path: string, content: string): Promise<void> { export async function writeFileText(
root: FileSystemDirectoryHandle,
path: string,
content: string
): Promise<void> {
const dirPerm = await ensureWritePerm(root); const dirPerm = await ensureWritePerm(root);
if (!dirPerm) throw new Error("No write permission"); if (!dirPerm) throw new Error("No write permission");
const fh = await getFileHandleByPath(root, path, true); const fh = await getFileHandleByPath(root, path, true);
@ -66,13 +98,20 @@ export async function writeFileText(root: FileSystemDirectoryHandle, path: strin
await w.close(); await w.close();
} }
export async function createDirectory(root: FileSystemDirectoryHandle, path: string): Promise<void> { export async function createDirectory(
root: FileSystemDirectoryHandle,
path: string
): Promise<void> {
const ok = await ensureWritePerm(root); const ok = await ensureWritePerm(root);
if (!ok) throw new Error("No write permission"); if (!ok) throw new Error("No write permission");
await getDirectoryHandleByPath(root, path, true); await getDirectoryHandleByPath(root, path, true);
} }
export async function deleteEntry(root: FileSystemDirectoryHandle, path: string, recursive = false): Promise<void> { export async function deleteEntry(
root: FileSystemDirectoryHandle,
path: string,
recursive = false
): Promise<void> {
const parts = path.split("/"); const parts = path.split("/");
const dirPath = parts.slice(0, -1).join("/"); const dirPath = parts.slice(0, -1).join("/");
const name = parts[parts.length - 1]; const name = parts[parts.length - 1];
@ -81,7 +120,10 @@ export async function deleteEntry(root: FileSystemDirectoryHandle, path: string,
await (dir as any).removeEntry(name, { recursive }).catch(() => undefined); await (dir as any).removeEntry(name, { recursive }).catch(() => undefined);
} }
export async function* walk(dir: FileSystemDirectoryHandle, pathPrefix = ""): AsyncGenerator<{ path: string; file: File }>{ export async function* walk(
dir: FileSystemDirectoryHandle,
pathPrefix = ""
): AsyncGenerator<{ path: string; file: File }> {
// @ts-ignore // @ts-ignore
for await (const [name, entry] of (dir as any).entries()) { for await (const [name, entry] of (dir as any).entries()) {
const p = pathPrefix ? `${pathPrefix}/${name}` : name; const p = pathPrefix ? `${pathPrefix}/${name}` : name;
@ -94,17 +136,24 @@ export async function* walk(dir: FileSystemDirectoryHandle, pathPrefix = ""): As
} }
} }
export async function copyDirectory(src: FileSystemDirectoryHandle, dest: FileSystemDirectoryHandle): Promise<void> { export async function copyDirectory(
src: FileSystemDirectoryHandle,
dest: FileSystemDirectoryHandle
): Promise<void> {
const ok = await ensureWritePerm(dest); const ok = await ensureWritePerm(dest);
if (!ok) throw new Error("No write permission on destination"); if (!ok) throw new Error("No write permission on destination");
// @ts-ignore // @ts-ignore
for await (const [name, entry] of (src as any).entries()) { for await (const [name, entry] of (src as any).entries()) {
if (entry.kind === "directory") { if (entry.kind === "directory") {
const sub = await (dest as any).getDirectoryHandle(name, { create: true }); const sub = await (dest as any).getDirectoryHandle(name, {
create: true,
});
await copyDirectory(entry as FileSystemDirectoryHandle, sub); await copyDirectory(entry as FileSystemDirectoryHandle, sub);
} else { } else {
const file = await (entry as FileSystemFileHandle).getFile(); const file = await (entry as FileSystemFileHandle).getFile();
const fh = await (dest as any).getFileHandle(name, { create: true }); const fh = await (dest as any).getFileHandle(name, {
create: true,
});
const w = await (fh as any).createWritable(); const w = await (fh as any).createWritable();
await w.write(await file.arrayBuffer()); await w.write(await file.arrayBuffer());
await w.close(); await w.close();

View file

@ -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 url = process.env.SUPABASE_URL as string;
const key = process.env.SUPABASE_ANON_KEY 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;

View file

@ -1,5 +1,12 @@
import { getAll, getAllByIndex, type RepoRecord, type BranchRecord, type CommitRecord, type IssueRecord } from './db'; import {
import { supabase } from './supabase'; getAll,
getAllByIndex,
type RepoRecord,
type BranchRecord,
type CommitRecord,
type IssueRecord,
} from "./db";
import { supabase } from "./supabase";
export interface ExportBundle { export interface ExportBundle {
repo: RepoRecord; repo: RepoRecord;
@ -10,27 +17,47 @@ export interface ExportBundle {
} }
export async function exportRepoBundle(repoId: string): Promise<ExportBundle> { export async function exportRepoBundle(repoId: string): Promise<ExportBundle> {
const repos = await getAll<RepoRecord>('repos'); const repos = await getAll<RepoRecord>("repos");
const repo = repos.find(r=>r.id===repoId)!; const repo = repos.find((r) => r.id === repoId)!;
const branches = await getAllByIndex<BranchRecord>('branches','by_repo', repoId); const branches = await getAllByIndex<BranchRecord>(
const commits = await getAllByIndex<CommitRecord>('commits','by_repo', repoId); "branches",
const issues = await getAllByIndex<IssueRecord>('issues','by_repo', repoId); "by_repo",
repoId
);
const commits = await getAllByIndex<CommitRecord>(
"commits",
"by_repo",
repoId
);
const issues = await getAllByIndex<IssueRecord>(
"issues",
"by_repo",
repoId
);
return { repo, branches, commits, issues, exportedAt: Date.now() }; return { repo, branches, commits, issues, exportedAt: Date.now() };
} }
export async function pushRepoToSupabase(repoId: string): Promise<void> { export async function pushRepoToSupabase(repoId: string): Promise<void> {
if (!supabase) throw new Error('Supabase not configured'); if (!supabase) throw new Error("Supabase not configured");
const bundle = await exportRepoBundle(repoId); const bundle = await exportRepoBundle(repoId);
const path = `repos/${repoId}.json`; const path = `repos/${repoId}.json`;
const data = new Blob([JSON.stringify(bundle)], { type: 'application/json' }); const data = new Blob([JSON.stringify(bundle)], {
const { error } = await supabase.storage.from('switch').upload(path, data, { upsert: true }); type: "application/json",
});
const { error } = await supabase.storage
.from("switch")
.upload(path, data, { upsert: true });
if (error) throw error; if (error) throw error;
} }
export async function pullRepoFromSupabase(repoId: string): Promise<ExportBundle | undefined> { export async function pullRepoFromSupabase(
if (!supabase) throw new Error('Supabase not configured'); repoId: string
): Promise<ExportBundle | undefined> {
if (!supabase) throw new Error("Supabase not configured");
const path = `repos/${repoId}.json`; const path = `repos/${repoId}.json`;
const { data, error } = await supabase.storage.from('switch').download(path); const { data, error } = await supabase.storage
.from("switch")
.download(path);
if (error) throw error; if (error) throw error;
const text = await data.text(); const text = await data.text();
return JSON.parse(text) as ExportBundle; return JSON.parse(text) as ExportBundle;

View file

@ -1,7 +1,17 @@
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"; import { nanoid } from "./uid";
export async function createRepoTemplate(name: string, fsHandleId: string): Promise<RepoTemplateRecord> { export async function createRepoTemplate(
name: string,
fsHandleId: string
): Promise<RepoTemplateRecord> {
const rec: RepoTemplateRecord = { id: nanoid(), name, fsHandleId }; const rec: RepoTemplateRecord = { id: nanoid(), name, fsHandleId };
await put("repoTemplates" as any, rec as any); await put("repoTemplates" as any, rec as any);
return rec; return rec;
@ -13,7 +23,12 @@ export async function deleteRepoTemplate(id: string): Promise<void> {
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<IssueTemplateRecord> { export async function createIssueTemplate(data: {
name: string;
title: string;
body: string;
labels: string[];
}): Promise<IssueTemplateRecord> {
const rec: IssueTemplateRecord = { id: nanoid(), ...data }; const rec: IssueTemplateRecord = { id: nanoid(), ...data };
await put("issueTemplates" as any, rec as any); await put("issueTemplates" as any, rec as any);
return rec; return rec;

View file

@ -76,7 +76,9 @@ module.exports = {
"process.env": { "process.env": {
YEAR: JSON.stringify(new Date().getFullYear()), YEAR: JSON.stringify(new Date().getFullYear()),
SUPABASE_URL: JSON.stringify(process.env.SUPABASE_URL || ""), 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(), new CleanWebpackPlugin(),
@ -119,7 +121,11 @@ module.exports = {
}), }),
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
{ from: "./typedoc/dist", to: "./typedoc/", noErrorOnMissing: true }, {
from: "./typedoc/dist",
to: "./typedoc/",
noErrorOnMissing: true,
},
], ],
}), }),
new CopyPlugin({ new CopyPlugin({