From c81513b0ea0f56faa29a28c8caf6becfd73afa37 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 3 Mar 2026 08:05:27 -0800 Subject: [PATCH 1/2] Add repo visibility toggle to git app --- apps/git/index.tsx | 149 ++++++++++++++++++++++++++++++++++++------ apps/git/package.json | 1 + 2 files changed, 130 insertions(+), 20 deletions(-) diff --git a/apps/git/index.tsx b/apps/git/index.tsx index 7c40042..076d971 100644 --- a/apps/git/index.tsx +++ b/apps/git/index.tsx @@ -2,17 +2,36 @@ import { Hype } from '@because/hype' import { define, stylesToCSS } from '@because/forge' import { baseStyles, ToolScript, theme, on } from '@because/toes/tools' import { mkdirSync } from 'fs' -import { mkdir, readdir, rename, rm, stat } from 'fs/promises' +import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'fs/promises' import { join, resolve } from 'path' import type { Child } from 'hono/jsx' const APP_URL = process.env.APP_URL! const APPS_DIR = process.env.APPS_DIR! +const DATA_DIR = process.env.DATA_DIR! const DATA_ROOT = process.env.DATA_ROOT! const TOES_URL = process.env.TOES_URL! const REPOS_DIR = resolve(DATA_ROOT, 'repos') const VALID_NAME = /^[a-zA-Z0-9_-]+$/ +const VISIBILITY_PATH = join(DATA_DIR, 'visibility.json') + +const TOGGLE_SCRIPT = ` +function toggleVisibility(btn) { + var repo = btn.dataset.repo; + var current = btn.dataset.visibility; + var next = current === 'public' ? 'private' : 'public'; + btn.dataset.visibility = next; + btn.textContent = next; + btn.className = btn.className.replace(' public', ''); + if (next === 'public') btn.className += ' public'; + fetch('/api/visibility/' + encodeURIComponent(repo), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ visibility: next }) + }); +} +` const app = new Hype({ prettyHTML: false, layout: false }) const deployLocks = new Map>() @@ -92,6 +111,29 @@ const RepoName = define('RepoName', { color: theme('colors-text'), }) +const Toggle = define('Toggle', { + base: 'button', + display: 'inline-flex', + alignItems: 'center', + gap: '6px', + padding: '3px 10px', + borderRadius: theme('radius-md'), + border: `1px solid ${theme('colors-border')}`, + backgroundColor: theme('colors-bgElement'), + color: theme('colors-textMuted'), + fontSize: '12px', + cursor: 'pointer', + transition: 'all 0.15s ease', + states: { + ':hover': { borderColor: theme('colors-textMuted') }, + '.public': { + backgroundColor: theme('colors-statusRunning'), + color: 'white', + borderColor: 'transparent', + }, + }, +}) + // --------------------------------------------------------------------------- // Interfaces // --------------------------------------------------------------------------- @@ -111,9 +153,12 @@ interface LayoutProps { interface RepoListPageProps { baseUrl: string - repos: Array<{ name: string; commits: boolean; branch: string }> + external: boolean + repos: Array<{ name: string; commits: boolean; branch: string; visibility: Visibility }> } +type Visibility = 'public' | 'private' + // --------------------------------------------------------------------------- // Functions // --------------------------------------------------------------------------- @@ -235,6 +280,11 @@ function findLastFlush(data: Uint8Array): number { return -1 } +async function getVisibility(repo: string): Promise { + const all = await loadVisibility() + return all[repo] ?? 'private' +} + async function getDefaultBranch(bare: string): Promise { const proc = Bun.spawn(['git', 'symbolic-ref', 'HEAD'], { cwd: bare, @@ -340,6 +390,15 @@ function insertBeforeFlush(gitBody: Uint8Array, msg: Uint8Array): Uint8Array { return out } +async function loadVisibility(): Promise> { + try { + const data = await readFile(VISIBILITY_PATH, 'utf-8') + return JSON.parse(data) + } catch { + return {} + } +} + function Layout({ title, children }: LayoutProps) { return ( @@ -373,6 +432,12 @@ function serviceHeader(service: string): Uint8Array { return new TextEncoder().encode(header) } +async function saveVisibility(repo: string, visibility: Visibility): Promise { + const all = await loadVisibility() + all[repo] = visibility + await writeFile(VISIBILITY_PATH, JSON.stringify(all, null, 2)) +} + async function stopIfRunning(name: string): Promise { const res = await fetch(`${TOES_URL}/api/apps/${name}`) if (!res.ok) return @@ -476,29 +541,33 @@ function AppRepo({ appName, baseUrl, branch, exists, commits }: AppRepoProps) { ) } -function RepoListPage({ baseUrl, repos }: RepoListPageProps) { +function RepoListPage({ baseUrl, external, repos }: RepoListPageProps) { return ( - Push to Deploy - - Push a git repository to deploy it as a toes app. - The repo must contain a package.json with a scripts.toes entry. - + {!external && ( + <> + Push to Deploy + + Push a git repository to deploy it as a toes app. + The repo must contain a package.json with a scripts.toes entry. + - {[ - '# Add this server as a remote and push', - `git remote add toes ${baseUrl}/`, - 'git push toes main', - '', - '# Or push an existing repo', - `git push ${baseUrl}/ main`, - ].join('\n')} + {[ + '# Add this server as a remote and push', + `git remote add toes ${baseUrl}/`, + 'git push toes main', + '', + '# Or push an existing repo', + `git push ${baseUrl}/ main`, + ].join('\n')} + + )} {repos.length > 0 && ( <> Repositories - {repos.map(({ name, commits, branch }) => ( + {repos.map(({ name, commits, branch, visibility }) => (
{name} @@ -507,6 +576,16 @@ function RepoListPage({ baseUrl, repos }: RepoListPageProps) {
+ {!external && ( + + {visibility === 'public' ? 'public' : 'private'} + + )} {branch} {commits ? deployed @@ -515,6 +594,7 @@ function RepoListPage({ baseUrl, repos }: RepoListPageProps) { ))} + {!external &&