Compare commits
No commits in common. "002f0a64ef83423ec482edfcdeb1ba5e33282098" and "d29ab8e37f1ec1dac7fe7ec929887ebfbfcc15d1" have entirely different histories.
002f0a64ef
...
d29ab8e37f
|
|
@ -2,39 +2,17 @@ import { Hype } from '@because/hype'
|
||||||
import { define, stylesToCSS } from '@because/forge'
|
import { define, stylesToCSS } from '@because/forge'
|
||||||
import { baseStyles, ToolScript, theme, on } from '@because/toes/tools'
|
import { baseStyles, ToolScript, theme, on } from '@because/toes/tools'
|
||||||
import { mkdirSync } from 'fs'
|
import { mkdirSync } from 'fs'
|
||||||
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'fs/promises'
|
import { mkdir, readdir, rename, rm, stat } from 'fs/promises'
|
||||||
import { join, resolve } from 'path'
|
import { join, resolve } from 'path'
|
||||||
import type { Child } from 'hono/jsx'
|
import type { Child } from 'hono/jsx'
|
||||||
|
|
||||||
const APP_URL = process.env.APP_URL!
|
const APP_URL = process.env.APP_URL!
|
||||||
const APPS_DIR = process.env.APPS_DIR!
|
const APPS_DIR = process.env.APPS_DIR!
|
||||||
const DATA_DIR = process.env.DATA_DIR!
|
|
||||||
const DATA_ROOT = process.env.DATA_ROOT!
|
const DATA_ROOT = process.env.DATA_ROOT!
|
||||||
const TOES_URL = process.env.TOES_URL!
|
const TOES_URL = process.env.TOES_URL!
|
||||||
|
|
||||||
const REPOS_DIR = resolve(DATA_ROOT, 'repos')
|
const REPOS_DIR = resolve(DATA_ROOT, 'repos')
|
||||||
const VALID_NAME = /^[a-zA-Z0-9_-]+$/
|
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.classList.toggle('public', next === 'public');
|
|
||||||
fetch('/api/visibility/' + encodeURIComponent(repo), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ visibility: next })
|
|
||||||
}).catch(function() {
|
|
||||||
btn.dataset.visibility = current;
|
|
||||||
btn.textContent = current;
|
|
||||||
btn.classList.toggle('public', current === 'public');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const app = new Hype({ prettyHTML: false, layout: false })
|
const app = new Hype({ prettyHTML: false, layout: false })
|
||||||
const deployLocks = new Map<string, Promise<void>>()
|
const deployLocks = new Map<string, Promise<void>>()
|
||||||
|
|
@ -114,29 +92,6 @@ const RepoName = define('RepoName', {
|
||||||
color: theme('colors-text'),
|
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
|
// Interfaces
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -156,12 +111,9 @@ interface LayoutProps {
|
||||||
|
|
||||||
interface RepoListPageProps {
|
interface RepoListPageProps {
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
external: boolean
|
repos: Array<{ name: string; commits: boolean; branch: string }>
|
||||||
repos: Array<{ name: string; commits: boolean; branch: string; visibility: Visibility }>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Visibility = 'public' | 'private'
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Functions
|
// Functions
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -283,11 +235,6 @@ function findLastFlush(data: Uint8Array): number {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVisibility(repo: string): Promise<Visibility> {
|
|
||||||
const all = await loadVisibility()
|
|
||||||
return all[repo] ?? 'private'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDefaultBranch(bare: string): Promise<string> {
|
async function getDefaultBranch(bare: string): Promise<string> {
|
||||||
const proc = Bun.spawn(['git', 'symbolic-ref', 'HEAD'], {
|
const proc = Bun.spawn(['git', 'symbolic-ref', 'HEAD'], {
|
||||||
cwd: bare,
|
cwd: bare,
|
||||||
|
|
@ -393,15 +340,6 @@ function insertBeforeFlush(gitBody: Uint8Array, msg: Uint8Array): Uint8Array {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadVisibility(): Promise<Record<string, Visibility>> {
|
|
||||||
try {
|
|
||||||
const data = await readFile(VISIBILITY_PATH, 'utf-8')
|
|
||||||
return JSON.parse(data)
|
|
||||||
} catch {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Layout({ title, children }: LayoutProps) {
|
function Layout({ title, children }: LayoutProps) {
|
||||||
return (
|
return (
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -435,12 +373,6 @@ function serviceHeader(service: string): Uint8Array {
|
||||||
return new TextEncoder().encode(header)
|
return new TextEncoder().encode(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveVisibility(repo: string, visibility: Visibility): Promise<void> {
|
|
||||||
const all = await loadVisibility()
|
|
||||||
all[repo] = visibility
|
|
||||||
await writeFile(VISIBILITY_PATH, JSON.stringify(all, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopIfRunning(name: string): Promise<void> {
|
async function stopIfRunning(name: string): Promise<void> {
|
||||||
const res = await fetch(`${TOES_URL}/api/apps/${name}`)
|
const res = await fetch(`${TOES_URL}/api/apps/${name}`)
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
|
|
@ -544,11 +476,9 @@ function AppRepo({ appName, baseUrl, branch, exists, commits }: AppRepoProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepoListPage({ baseUrl, external, repos }: RepoListPageProps) {
|
function RepoListPage({ baseUrl, repos }: RepoListPageProps) {
|
||||||
return (
|
return (
|
||||||
<Layout title="Git">
|
<Layout title="Git">
|
||||||
{!external && (
|
|
||||||
<>
|
|
||||||
<Heading>Push to Deploy</Heading>
|
<Heading>Push to Deploy</Heading>
|
||||||
<HelpText>
|
<HelpText>
|
||||||
Push a git repository to deploy it as a toes app.
|
Push a git repository to deploy it as a toes app.
|
||||||
|
|
@ -563,14 +493,12 @@ function RepoListPage({ baseUrl, external, repos }: RepoListPageProps) {
|
||||||
'# Or push an existing repo',
|
'# Or push an existing repo',
|
||||||
`git push ${baseUrl}/<app-name> main`,
|
`git push ${baseUrl}/<app-name> main`,
|
||||||
].join('\n')}</CodeBlock>
|
].join('\n')}</CodeBlock>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{repos.length > 0 && (
|
{repos.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Heading>Repositories</Heading>
|
<Heading>Repositories</Heading>
|
||||||
<RepoList>
|
<RepoList>
|
||||||
{repos.map(({ name, commits, branch, visibility }) => (
|
{repos.map(({ name, commits, branch }) => (
|
||||||
<RepoItem>
|
<RepoItem>
|
||||||
<div>
|
<div>
|
||||||
<RepoName>{name}</RepoName>
|
<RepoName>{name}</RepoName>
|
||||||
|
|
@ -579,16 +507,6 @@ function RepoListPage({ baseUrl, external, repos }: RepoListPageProps) {
|
||||||
</HelpText>
|
</HelpText>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 8px; align-items: center">
|
<div style="display: flex; gap: 8px; align-items: center">
|
||||||
{!external && (
|
|
||||||
<Toggle
|
|
||||||
class={visibility === 'public' ? 'public' : ''}
|
|
||||||
data-repo={name}
|
|
||||||
data-visibility={visibility}
|
|
||||||
onclick="toggleVisibility(this)"
|
|
||||||
>
|
|
||||||
{visibility === 'public' ? 'public' : 'private'}
|
|
||||||
</Toggle>
|
|
||||||
)}
|
|
||||||
<Badge>{branch}</Badge>
|
<Badge>{branch}</Badge>
|
||||||
{commits
|
{commits
|
||||||
? <Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
|
? <Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
|
||||||
|
|
@ -597,7 +515,6 @@ function RepoListPage({ baseUrl, external, repos }: RepoListPageProps) {
|
||||||
</RepoItem>
|
</RepoItem>
|
||||||
))}
|
))}
|
||||||
</RepoList>
|
</RepoList>
|
||||||
{!external && <script src="/toggle.js" />}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -625,10 +542,6 @@ app.get('/styles.css', c =>
|
||||||
c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }),
|
c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }),
|
||||||
)
|
)
|
||||||
|
|
||||||
app.get('/toggle.js', c =>
|
|
||||||
c.text(TOGGLE_SCRIPT, 200, { 'Content-Type': 'application/javascript; charset=utf-8' }),
|
|
||||||
)
|
|
||||||
|
|
||||||
// GET /:repo[.git]/info/refs?service=git-upload-pack|git-receive-pack
|
// GET /:repo[.git]/info/refs?service=git-upload-pack|git-receive-pack
|
||||||
app.on('GET', ['/:repo{.+\\.git}/info/refs', '/:repo/info/refs'], async c => {
|
app.on('GET', ['/:repo{.+\\.git}/info/refs', '/:repo/info/refs'], async c => {
|
||||||
const repoParam = c.req.param('repo').replace(/\.git$/, '')
|
const repoParam = c.req.param('repo').replace(/\.git$/, '')
|
||||||
|
|
@ -732,25 +645,9 @@ app.on('POST', ['/:repo{.+\\.git}/git-receive-pack', '/:repo/git-receive-pack'],
|
||||||
return new Response(gitBody, { headers })
|
return new Response(gitBody, { headers })
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/visibility/:repo', async c => {
|
|
||||||
if (c.req.header('x-sneaker')) return c.json({ error: 'Forbidden' }, 403)
|
|
||||||
|
|
||||||
const repo = c.req.param('repo')
|
|
||||||
if (!validRepoName(repo)) return c.json({ error: 'Invalid repository name' }, 400)
|
|
||||||
|
|
||||||
const body = await c.req.json<{ visibility: string }>()
|
|
||||||
if (body.visibility !== 'public' && body.visibility !== 'private') {
|
|
||||||
return c.json({ error: 'Visibility must be "public" or "private"' }, 400)
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveVisibility(repo, body.visibility)
|
|
||||||
return c.json({ ok: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/', async c => {
|
app.get('/', async c => {
|
||||||
const appName = c.req.query('app')
|
const appName = c.req.query('app')
|
||||||
const baseUrl = APP_URL
|
const baseUrl = APP_URL
|
||||||
const external = !!c.req.header('x-sneaker')
|
|
||||||
|
|
||||||
// When viewing a specific app, only show that app's repo
|
// When viewing a specific app, only show that app's repo
|
||||||
if (appName) {
|
if (appName) {
|
||||||
|
|
@ -767,20 +664,11 @@ app.get('/', async c => {
|
||||||
const repos = await listRepos()
|
const repos = await listRepos()
|
||||||
const repoData = await Promise.all(repos.map(async name => {
|
const repoData = await Promise.all(repos.map(async name => {
|
||||||
const bare = repoPath(name)
|
const bare = repoPath(name)
|
||||||
const [commits, branch, visibility] = await Promise.all([
|
const [commits, branch] = await Promise.all([hasCommits(bare), getDefaultBranch(bare)])
|
||||||
hasCommits(bare),
|
return { name, commits, branch }
|
||||||
getDefaultBranch(bare),
|
|
||||||
getVisibility(name),
|
|
||||||
])
|
|
||||||
return { name, commits, branch, visibility }
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Hide private repos from external (sneaker) requests
|
return c.html(<RepoListPage baseUrl={baseUrl} repos={repoData} />)
|
||||||
const filtered = external
|
|
||||||
? repoData.filter(r => r.visibility === 'public')
|
|
||||||
: repoData
|
|
||||||
|
|
||||||
return c.html(<RepoListPage baseUrl={baseUrl} external={external} repos={filtered} />)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default app.defaults
|
export default app.defaults
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
},
|
},
|
||||||
"toes": {
|
"toes": {
|
||||||
"tool": true,
|
"tool": true,
|
||||||
"dashboard": true,
|
|
||||||
"icon": "🔀"
|
"icon": "🔀"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
"remote:restart": "./scripts/remote-restart.sh",
|
"remote:restart": "./scripts/remote-restart.sh",
|
||||||
"remote:start": "./scripts/remote-start.sh",
|
"remote:start": "./scripts/remote-start.sh",
|
||||||
"remote:stop": "./scripts/remote-stop.sh",
|
"remote:stop": "./scripts/remote-stop.sh",
|
||||||
"start": "bun run templates && bun run src/server/index.tsx",
|
"start": "bun run src/server/index.tsx",
|
||||||
"templates": "bun run scripts/embed-templates.ts",
|
"templates": "bun run scripts/embed-templates.ts",
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user