Compare commits
1 Commits
894ae7e497
...
9db13eaa6a
| Author | SHA1 | Date | |
|---|---|---|---|
| 9db13eaa6a |
|
|
@ -7,7 +7,7 @@
|
|||
"dependencies": {
|
||||
"@because/forge": "^0.0.1",
|
||||
"@because/hype": "^0.0.2",
|
||||
"@because/toes": "^0.0.12",
|
||||
"@because/toes": "0.0.9",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
|
|
@ -22,15 +22,15 @@
|
|||
|
||||
"@because/hype": ["@because/hype@0.0.2", "https://npm.nose.space/@because/hype/-/hype-0.0.2.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-fdKeII6USGC1loVVj+tPz086cKz+Bm+XozNee3NOnK4VP+q4yNPP2Fq1Yujw5xeDYE+ZvJn40gKwlngRvmX2hA=="],
|
||||
|
||||
"@because/sneaker": ["@because/sneaker@0.0.4", "https://npm.nose.space/@because/sneaker/-/sneaker-0.0.4.tgz", { "dependencies": { "hono": "^4.9.8", "unique-names-generator": "^4.7.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-juklirqLPOzCQTlY3Vf6elXO7bPTEfc1QB4ephdWONZwllovtAEF4H0O6CoOcoV5g5P0i8qUu+ffNVqtkC3SBw=="],
|
||||
"@because/sneaker": ["@because/sneaker@0.0.3", "https://npm.nose.space/@because/sneaker/-/sneaker-0.0.3.tgz", { "dependencies": { "hono": "^4.9.8", "unique-names-generator": "^4.7.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-4cG8w/tYPGbDtLw89k1PiASJKfWUdd1NXv+GKad2d7Ckw3FpZ+dnN2+gR2ihs81dqAkNaZomo+9RznBju2WaOw=="],
|
||||
|
||||
"@because/toes": ["@because/toes@0.0.12", "https://npm.nose.space/@because/toes/-/toes-0.0.12.tgz", { "dependencies": { "@because/forge": "^0.0.1", "@because/hype": "^0.0.2", "@because/sneaker": "^0.0.4", "commander": "14.0.3", "diff": "^8.0.3", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5.9.3" }, "bin": { "toes": "src/cli/index.ts" } }, "sha512-jJu2hU/QmFZ2mNQZg6Z/gqbRUU4twMn+jPIijk7+UMzU4spbUa4pmNkr+zVlBPo38Sx7edHxf3F0SAjoYkEbaQ=="],
|
||||
"@because/toes": ["@because/toes@0.0.9", "https://npm.nose.space/@because/toes/-/toes-0.0.9.tgz", { "dependencies": { "@because/forge": "^0.0.1", "@because/hype": "^0.0.2", "@because/sneaker": "^0.0.3", "commander": "14.0.3", "diff": "^8.0.3", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5.9.3" }, "bin": { "toes": "src/cli/index.ts" } }, "sha512-QkRTPLkPW8awH1DC0vqFR9oS3q+xOZ00eL6VuFElM1fatqHnLO8zKKJcyvy1pU8ZKS4Ev7F+OoyBzRuo6OTa/g=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.10", "https://npm.nose.space/@types/bun/-/bun-1.3.10.tgz", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||
"@types/bun": ["@types/bun@1.3.9", "https://npm.nose.space/@types/bun/-/bun-1.3.9.tgz", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.3", "https://npm.nose.space/@types/node/-/node-25.3.3.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.10", "https://npm.nose.space/bun-types/-/bun-types-1.3.10.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||
"bun-types": ["bun-types@1.3.9", "https://npm.nose.space/bun-types/-/bun-types-1.3.9.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||
|
||||
"commander": ["commander@14.0.3", "https://npm.nose.space/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
[install]
|
||||
registry = "https://npm.nose.space"
|
||||
|
|
@ -15,6 +15,26 @@ const TOES_URL = process.env.TOES_URL!
|
|||
const REPOS_DIR = resolve(DATA_ROOT, 'repos')
|
||||
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 deployLocks = new Map<string, Promise<void>>()
|
||||
|
||||
|
|
@ -93,31 +113,6 @@ const RepoName = define('RepoName', {
|
|||
color: theme('colors-text'),
|
||||
})
|
||||
|
||||
const Tab = define('Tab', {
|
||||
base: 'button',
|
||||
padding: '6px 0',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
borderBottom: '2px solid transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
color: theme('colors-textMuted'),
|
||||
states: {
|
||||
':hover': { color: theme('colors-text') },
|
||||
'.active': {
|
||||
color: theme('colors-text'),
|
||||
borderBottomColor: theme('colors-primary'),
|
||||
fontWeight: '500',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const TabBar = define('TabBar', {
|
||||
display: 'flex',
|
||||
gap: '24px',
|
||||
marginBottom: '20px',
|
||||
})
|
||||
|
||||
const Toggle = define('Toggle', {
|
||||
base: 'button',
|
||||
display: 'inline-flex',
|
||||
|
|
@ -161,8 +156,7 @@ interface LayoutProps {
|
|||
interface RepoListPageProps {
|
||||
baseUrl: string
|
||||
external: boolean
|
||||
repos: Array<{ name: string; commits: boolean; branch: string; visibility: Visibility; tool: boolean }>
|
||||
tunnelUrl?: string
|
||||
repos: Array<{ name: string; commits: boolean; branch: string; visibility: Visibility }>
|
||||
}
|
||||
|
||||
type Visibility = 'public' | 'private'
|
||||
|
|
@ -549,56 +543,7 @@ function AppRepo({ appName, baseUrl, branch, exists, commits }: AppRepoProps) {
|
|||
)
|
||||
}
|
||||
|
||||
function RepoListItems({ baseUrl, external, repos, tunnelUrl }: {
|
||||
baseUrl: string
|
||||
external: boolean
|
||||
repos: RepoListPageProps['repos']
|
||||
tunnelUrl?: string
|
||||
}) {
|
||||
if (repos.length === 0) {
|
||||
return <HelpText>No repositories yet.</HelpText>
|
||||
}
|
||||
return (
|
||||
<RepoList>
|
||||
{repos.map(({ name, commits, branch, visibility }) => (
|
||||
<RepoItem>
|
||||
<div>
|
||||
<RepoName>{name}</RepoName>
|
||||
<HelpText style="margin: 4px 0 0; font-size: 12px">
|
||||
git clone {baseUrl}/{name}
|
||||
</HelpText>
|
||||
{!external && tunnelUrl && visibility === 'public' && (
|
||||
<HelpText style="margin: 2px 0 0; font-size: 12px">
|
||||
git clone {tunnelUrl}/{name}
|
||||
</HelpText>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
{commits
|
||||
? <Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
|
||||
: <Badge>empty</Badge>}
|
||||
</div>
|
||||
</RepoItem>
|
||||
))}
|
||||
</RepoList>
|
||||
)
|
||||
}
|
||||
|
||||
function RepoListPage({ baseUrl, external, repos, tunnelUrl }: RepoListPageProps) {
|
||||
const appRepos = repos.filter(r => !r.tool)
|
||||
const toolRepos = repos.filter(r => r.tool)
|
||||
|
||||
function RepoListPage({ baseUrl, external, repos }: RepoListPageProps) {
|
||||
return (
|
||||
<Layout title="Git">
|
||||
{!external && (
|
||||
|
|
@ -620,31 +565,38 @@ function RepoListPage({ baseUrl, external, repos, tunnelUrl }: RepoListPageProps
|
|||
</>
|
||||
)}
|
||||
|
||||
{repos.length > 0 && appRepos.length > 0 && toolRepos.length > 0 && (
|
||||
{repos.length > 0 && (
|
||||
<>
|
||||
<Heading>Repositories</Heading>
|
||||
<TabBar>
|
||||
<Tab class="active" data-tab="tab-apps" onclick="switchTab(this)">Apps</Tab>
|
||||
<Tab data-tab="tab-tools" onclick="switchTab(this)">Tools</Tab>
|
||||
</TabBar>
|
||||
<div>
|
||||
<div id="tab-apps">
|
||||
<RepoListItems baseUrl={baseUrl} external={external} repos={appRepos} tunnelUrl={tunnelUrl} />
|
||||
</div>
|
||||
<div id="tab-tools" style="display: none">
|
||||
<RepoListItems baseUrl={baseUrl} external={external} repos={toolRepos} tunnelUrl={tunnelUrl} />
|
||||
</div>
|
||||
</div>
|
||||
{!external && <script src="/client/toggle.js" />}
|
||||
<script src="/client/tabs.js" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{repos.length > 0 && (appRepos.length === 0 || toolRepos.length === 0) && (
|
||||
<>
|
||||
<Heading>Repositories</Heading>
|
||||
<RepoListItems baseUrl={baseUrl} external={external} repos={repos} tunnelUrl={tunnelUrl} />
|
||||
{!external && <script src="/client/toggle.js" />}
|
||||
<RepoList>
|
||||
{repos.map(({ name, commits, branch, visibility }) => (
|
||||
<RepoItem>
|
||||
<div>
|
||||
<RepoName>{name}</RepoName>
|
||||
<HelpText style="margin: 4px 0 0; font-size: 12px">
|
||||
git clone {baseUrl}/{name}
|
||||
</HelpText>
|
||||
</div>
|
||||
<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>
|
||||
{commits
|
||||
? <Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
|
||||
: <Badge>empty</Badge>}
|
||||
</div>
|
||||
</RepoItem>
|
||||
))}
|
||||
</RepoList>
|
||||
{!external && <script src="/toggle.js" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
@ -672,6 +624,9 @@ app.get('/styles.css', c =>
|
|||
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
|
||||
app.on('GET', ['/:repo{.+\\.git}/info/refs', '/:repo/info/refs'], async c => {
|
||||
|
|
@ -686,13 +641,8 @@ app.on('GET', ['/:repo{.+\\.git}/info/refs', '/:repo/info/refs'], async c => {
|
|||
return c.text('Invalid service', 400)
|
||||
}
|
||||
|
||||
if (c.req.header('x-sneaker')) {
|
||||
if (service === 'git-receive-pack') {
|
||||
return c.text('Push access denied over sneaker', 403)
|
||||
}
|
||||
if (await getVisibility(repoParam) !== 'public') {
|
||||
return c.text('Repository not found', 404)
|
||||
}
|
||||
if (service === 'git-receive-pack' && c.req.header('x-sneaker')) {
|
||||
return c.text('Push access denied over sneaker', 403)
|
||||
}
|
||||
|
||||
if (service === 'git-receive-pack') {
|
||||
|
|
@ -716,10 +666,6 @@ app.on('POST', ['/:repo{.+\\.git}/git-upload-pack', '/:repo/git-upload-pack'], a
|
|||
return c.text('Invalid repository name', 400)
|
||||
}
|
||||
|
||||
if (c.req.header('x-sneaker') && await getVisibility(repoParam) !== 'public') {
|
||||
return c.text('Repository not found', 404)
|
||||
}
|
||||
|
||||
const bare = repoPath(repoParam)
|
||||
if (!(await dirExists(bare))) {
|
||||
return c.text('Repository not found', 404)
|
||||
|
|
@ -802,9 +748,8 @@ app.post('/api/visibility/:repo', async c => {
|
|||
|
||||
app.get('/', async c => {
|
||||
const appName = c.req.query('app')
|
||||
const sneakerHost = c.req.header('x-sneaker')
|
||||
const external = !!sneakerHost
|
||||
const baseUrl = sneakerHost ? `https://${sneakerHost}` : APP_URL
|
||||
const baseUrl = APP_URL
|
||||
const external = !!c.req.header('x-sneaker')
|
||||
|
||||
// When viewing a specific app, only show that app's repo
|
||||
if (appName) {
|
||||
|
|
@ -819,19 +764,6 @@ app.get('/', async c => {
|
|||
|
||||
// No app selected — show all repos
|
||||
const repos = await listRepos()
|
||||
|
||||
// Fetch all apps to determine which repos are tools
|
||||
let toolSet = new Set<string>()
|
||||
try {
|
||||
const res = await fetch(`${TOES_URL}/api/apps`)
|
||||
if (res.ok) {
|
||||
const apps = await res.json() as Array<{ name: string; tool?: boolean | string }>
|
||||
for (const a of apps) {
|
||||
if (a.tool) toolSet.add(a.name)
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const repoData = await Promise.all(repos.map(async name => {
|
||||
const bare = repoPath(name)
|
||||
const [commits, branch, visibility] = await Promise.all([
|
||||
|
|
@ -839,7 +771,7 @@ app.get('/', async c => {
|
|||
getDefaultBranch(bare),
|
||||
getVisibility(name),
|
||||
])
|
||||
return { name, commits, branch, visibility, tool: toolSet.has(name) }
|
||||
return { name, commits, branch, visibility }
|
||||
}))
|
||||
|
||||
// Hide private repos from external (sneaker) requests
|
||||
|
|
@ -847,19 +779,7 @@ app.get('/', async c => {
|
|||
? repoData.filter(r => r.visibility === 'public')
|
||||
: repoData
|
||||
|
||||
// Fetch tunnel URL for the git tool so we can show it for public repos
|
||||
let tunnelUrl: string | undefined
|
||||
if (!external) {
|
||||
try {
|
||||
const res = await fetch(`${TOES_URL}/api/apps/git`)
|
||||
if (res.ok) {
|
||||
const info = await res.json() as { tunnelUrl?: string }
|
||||
tunnelUrl = info.tunnelUrl
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return c.html(<RepoListPage baseUrl={baseUrl} external={external} repos={filtered} tunnelUrl={tunnelUrl} />)
|
||||
return c.html(<RepoListPage baseUrl={baseUrl} external={external} repos={filtered} />)
|
||||
})
|
||||
|
||||
export default app.defaults
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
function switchTab(btn: HTMLButtonElement) {
|
||||
const tabs = btn.parentElement!.querySelectorAll('button')
|
||||
for (const tab of tabs) tab.classList.remove('active')
|
||||
btn.classList.add('active')
|
||||
|
||||
const panels = btn.parentElement!.nextElementSibling!.children
|
||||
for (const panel of panels) (panel as HTMLElement).style.display = 'none'
|
||||
|
||||
const target = document.getElementById(btn.dataset.tab!)
|
||||
if (target) target.style.display = 'block'
|
||||
}
|
||||
|
||||
Object.assign(window, { switchTab })
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
function toggleVisibility(btn: HTMLButtonElement) {
|
||||
const repo = btn.dataset.repo!
|
||||
const current = btn.dataset.visibility!
|
||||
const 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(() => {
|
||||
btn.dataset.visibility = current
|
||||
btn.textContent = current
|
||||
btn.classList.toggle('public', current === 'public')
|
||||
})
|
||||
}
|
||||
|
||||
Object.assign(window, { toggleVisibility })
|
||||
3
bun.lock
3
bun.lock
|
|
@ -8,7 +8,6 @@
|
|||
"@because/forge": "^0.0.1",
|
||||
"@because/hype": "^0.0.2",
|
||||
"@because/sneaker": "^0.0.4",
|
||||
"@because/toes": "^0.0.12",
|
||||
"commander": "14.0.3",
|
||||
"diff": "^8.0.3",
|
||||
"kleur": "^4.1.5",
|
||||
|
|
@ -29,8 +28,6 @@
|
|||
|
||||
"@because/sneaker": ["@because/sneaker@0.0.4", "https://npm.nose.space/@because/sneaker/-/sneaker-0.0.4.tgz", { "dependencies": { "hono": "^4.9.8", "unique-names-generator": "^4.7.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-juklirqLPOzCQTlY3Vf6elXO7bPTEfc1QB4ephdWONZwllovtAEF4H0O6CoOcoV5g5P0i8qUu+ffNVqtkC3SBw=="],
|
||||
|
||||
"@because/toes": ["@because/toes@0.0.12", "https://npm.nose.space/@because/toes/-/toes-0.0.12.tgz", { "dependencies": { "@because/forge": "^0.0.1", "@because/hype": "^0.0.2", "@because/sneaker": "^0.0.4", "commander": "14.0.3", "diff": "^8.0.3", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5.9.3" }, "bin": { "toes": "src/cli/index.ts" } }, "sha512-jJu2hU/QmFZ2mNQZg6Z/gqbRUU4twMn+jPIijk7+UMzU4spbUa4pmNkr+zVlBPo38Sx7edHxf3F0SAjoYkEbaQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.10", "https://npm.nose.space/@types/bun/-/bun-1.3.10.tgz", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||
|
||||
"@types/diff": ["@types/diff@8.0.0", "https://npm.nose.space/@types/diff/-/diff-8.0.0.tgz", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="],
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
b=$'\033[1m' d=$'\033[2m' g=$'\033[32m' c=$'\033[36m' y=$'\033[33m' r=$'\033[0m'
|
||||
|
||||
echo ""
|
||||
echo " ┌── CLI installer (curl | bash) ──────────────┐"
|
||||
echo ""
|
||||
echo " ${b}🐾 toes cli${r}"
|
||||
echo " ${d}─────────────────────────────${r}"
|
||||
echo ""
|
||||
echo " ${d}Fetching macos/arm64...${r}"
|
||||
echo " ${g}Installed to${r} ${b}/Users/chris/.local/bin/toes${r}"
|
||||
echo ""
|
||||
echo " ${y}Add /Users/chris/.local/bin to your \$PATH, then:${r}"
|
||||
echo " Run ${c}toes${r} to get started."
|
||||
echo ""
|
||||
echo ""
|
||||
echo " ┌── After deploy ─────────────────────────────┐"
|
||||
echo ""
|
||||
echo " ${b}${g}🐾 Deployed${r} to ${b}pi@toes.local${r}"
|
||||
echo " ${d}─────────────────────────────${r}"
|
||||
echo ""
|
||||
echo " Dashboard: ${c}http://toes.local${r}"
|
||||
echo ""
|
||||
echo " ${d}Grab the CLI:${r}"
|
||||
echo " ${c}curl -fsSL http://toes.local/install | bash${r}"
|
||||
echo ""
|
||||
echo ""
|
||||
echo " ┌── After server install ─────────────────────┐"
|
||||
echo ""
|
||||
echo " ${d}╔══════════════════════════════════╗${r}"
|
||||
echo " ${d}║${r} ${b}🐾 toes${r} ${d}- personal web appliance ║${r}"
|
||||
echo " ${d}╚══════════════════════════════════╝${r}"
|
||||
echo ""
|
||||
echo " ${d}>>${r} Updating system packages"
|
||||
echo " ${d}>>${r} Installing bun"
|
||||
echo " ${d}>>${r} Building"
|
||||
echo ""
|
||||
echo " ${b}${g}🐾 toes abc1234 is up!${r}"
|
||||
echo " ${d}─────────────────────────────${r}"
|
||||
echo ""
|
||||
echo " Dashboard: ${c}http://toes.local${r}"
|
||||
echo ""
|
||||
echo " ${d}Grab the CLI:${r}"
|
||||
echo " ${c}curl -fsSL http://toes.local/install | bash${r}"
|
||||
echo ""
|
||||
|
|
@ -15,20 +15,18 @@ DATA_DIR=~/data
|
|||
|
||||
# ── Helpers ──────────────────────────────────────────────
|
||||
|
||||
b=$'\033[1m' d=$'\033[2m' g=$'\033[32m' c=$'\033[36m' y=$'\033[33m' r=$'\033[0m'
|
||||
|
||||
quiet() { "$@" > /dev/null 2>&1; }
|
||||
|
||||
info() { echo " ${d}>>${r} $1"; }
|
||||
info() { echo ">> $1"; }
|
||||
|
||||
fail() { echo " ${y}ERROR:${r} $1" >&2; exit 1; }
|
||||
fail() { echo "ERROR: $1" >&2; exit 1; }
|
||||
|
||||
# ── Preflight ────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo " ${d}╔══════════════════════════════════╗${r}"
|
||||
echo " ${d}║${r} ${b}🐾 toes${r} ${d}- personal web appliance ║${r}"
|
||||
echo " ${d}╚══════════════════════════════════╝${r}"
|
||||
echo " ╔══════════════════════════════════╗"
|
||||
echo " ║ 🐾 toes - personal web appliance ║"
|
||||
echo " ╚══════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
[ "$(whoami)" = "toes" ] || fail "Must be run as the 'toes' user."
|
||||
|
|
@ -127,11 +125,9 @@ sudo systemctl restart toes
|
|||
VERSION=$(git describe --tags --always 2>/dev/null || echo "unknown")
|
||||
|
||||
echo ""
|
||||
echo " ${b}${g}🐾 toes $VERSION is up!${r}"
|
||||
echo " ${d}─────────────────────────────${r}"
|
||||
echo " toes $VERSION is running!"
|
||||
echo " http://$(hostname).local"
|
||||
echo ""
|
||||
echo " Dashboard: ${c}http://$(hostname).local${r}"
|
||||
echo ""
|
||||
echo " ${d}Grab the CLI:${r}"
|
||||
echo " ${c}curl -fsSL http://$(hostname).local/install | bash${r}"
|
||||
echo " Install the CLI on your local machine:"
|
||||
echo " curl -fsSL http://$(hostname).local/install | bash"
|
||||
echo ""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@because/toes",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.10",
|
||||
"description": "personal web appliance - turn it on and forget about the cloud",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
|
|
@ -47,7 +47,6 @@
|
|||
"@because/forge": "^0.0.1",
|
||||
"@because/hype": "^0.0.2",
|
||||
"@because/sneaker": "^0.0.4",
|
||||
"@because/toes": "^0.0.12",
|
||||
"commander": "14.0.3",
|
||||
"diff": "^8.0.3",
|
||||
"kleur": "^4.1.5"
|
||||
|
|
|
|||
|
|
@ -57,14 +57,9 @@ done
|
|||
sudo systemctl restart toes.service
|
||||
SCRIPT
|
||||
|
||||
b=$'\033[1m' d=$'\033[2m' g=$'\033[32m' c=$'\033[36m' r=$'\033[0m'
|
||||
|
||||
echo "=> Deployed to $SSH_HOST"
|
||||
echo "=> Visit $URL"
|
||||
echo ""
|
||||
echo " ${b}${g}🐾 Deployed${r} to ${b}$SSH_HOST${r}"
|
||||
echo " ${d}─────────────────────────────${r}"
|
||||
echo ""
|
||||
echo " Dashboard: ${c}$URL${r}"
|
||||
echo ""
|
||||
echo " ${d}Grab the CLI:${r}"
|
||||
echo " ${c}curl -fsSL $URL/install | bash${r}"
|
||||
echo " Install the CLI on your local machine:"
|
||||
echo " curl -fsSL $URL/install | bash"
|
||||
echo ""
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export function AppDetail({ app, render }: { app: App, render: () => void }) {
|
|||
<ClickableAppName onClick={() => openRenameAppModal(app)}>{app.name}</ClickableAppName>
|
||||
</MainTitle>
|
||||
<HeaderActions>
|
||||
{(!app.tool || app.share) && (
|
||||
{!app.tool && (
|
||||
app.tunnelUrl
|
||||
? <Button onClick={() => { unshareApp(app.name) }}>Unshare</Button>
|
||||
: app.tunnelEnabled
|
||||
|
|
|
|||
|
|
@ -28,9 +28,10 @@ function convert(app: BackendApp): SharedApp {
|
|||
router.sse('/stream', (send) => {
|
||||
let queue = Promise.resolve()
|
||||
const broadcast = () => {
|
||||
const apps: SharedApp[] = allApps().map(app => ({
|
||||
...convert(app),
|
||||
logs: app.logs,
|
||||
const apps: SharedApp[] = allApps().map(({
|
||||
name, state, icon, error, port, started, logs, tool, apps: apps_, dashboard, tunnelEnabled, tunnelUrl
|
||||
}) => ({
|
||||
name, state, icon, error, port, started, logs, tool, apps: apps_, dashboard, tunnelEnabled, tunnelUrl,
|
||||
}))
|
||||
queue = queue.then(() => send(apps))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,11 @@ export function registerApp(dir: string) {
|
|||
|
||||
const { pkg, error } = loadApp(dir)
|
||||
const state: AppState = error ? 'invalid' : 'stopped'
|
||||
_apps.set(dir, buildApp(dir, pkg, state, error))
|
||||
const icon = pkg.toes?.icon ?? DEFAULT_EMOJI
|
||||
const tool = pkg.toes?.tool
|
||||
const apps = pkg.toes?.apps
|
||||
const dashboard = pkg.toes?.dashboard
|
||||
_apps.set(dir, { name: dir, state, icon, error, tool, apps, dashboard })
|
||||
update()
|
||||
emit({ type: 'app:create', app: dir })
|
||||
if (!error) {
|
||||
|
|
@ -297,15 +301,6 @@ export function updateAppIcon(dir: string, icon: string) {
|
|||
}
|
||||
}
|
||||
|
||||
const buildApp = (dir: string, pkg: any, state: AppState, error?: string): App => ({
|
||||
name: dir, state, error,
|
||||
icon: pkg.toes?.icon ?? DEFAULT_EMOJI,
|
||||
tool: pkg.toes?.tool,
|
||||
apps: pkg.toes?.apps,
|
||||
dashboard: pkg.toes?.dashboard,
|
||||
share: pkg.toes?.share,
|
||||
})
|
||||
|
||||
const clearTimers = (app: App) => {
|
||||
if (app.startupTimer) {
|
||||
clearTimeout(app.startupTimer)
|
||||
|
|
@ -354,7 +349,11 @@ function discoverApps() {
|
|||
for (const dir of allAppDirs()) {
|
||||
const { pkg, error } = loadApp(dir)
|
||||
const state: AppState = error ? 'invalid' : 'stopped'
|
||||
_apps.set(dir, buildApp(dir, pkg, state, error))
|
||||
const icon = pkg.toes?.icon ?? DEFAULT_EMOJI
|
||||
const tool = pkg.toes?.tool
|
||||
const apps = pkg.toes?.apps
|
||||
const dashboard = pkg.toes?.dashboard
|
||||
_apps.set(dir, { name: dir, state, icon, error, tool, apps, dashboard })
|
||||
}
|
||||
update()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
b=$'\033[1m' d=$'\033[2m' g=$'\033[32m' c=$'\033[36m' y=$'\033[33m' r=$'\033[0m'
|
||||
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS" in
|
||||
darwin) OS=macos ;;
|
||||
linux) OS=linux ;;
|
||||
*) echo "${y}Unsupported OS: $OS${r}" >&2; exit 1 ;;
|
||||
*) echo "🐾 Unsupported OS: $OS" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH=x64 ;;
|
||||
arm64|aarch64) ARCH=arm64 ;;
|
||||
*) echo "${y}Unsupported arch: $ARCH${r}" >&2; exit 1 ;;
|
||||
*) echo "🐾 Unsupported arch: $ARCH" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
BINARY="toes-${OS}-${ARCH}"
|
||||
|
|
@ -30,18 +28,11 @@ else
|
|||
fi
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
echo ""
|
||||
echo " ${b}🐾 toes cli${r}"
|
||||
echo " ${d}─────────────────────────────${r}"
|
||||
echo ""
|
||||
echo " ${d}Fetching ${OS}/${ARCH}...${r}"
|
||||
echo "🐾 Downloading toes CLI (${OS}/${ARCH})..."
|
||||
curl -fsSL "$URL" -o "$INSTALL_DIR/toes"
|
||||
chmod +x "$INSTALL_DIR/toes"
|
||||
echo " ${g}Installed to${r} ${b}$INSTALL_DIR/toes${r}"
|
||||
echo ""
|
||||
echo "🐾 Installed toes to $INSTALL_DIR/toes"
|
||||
|
||||
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
|
||||
echo " ${y}Add $INSTALL_DIR to your \$PATH, then:${r}"
|
||||
echo "🐾 Add $INSTALL_DIR to your PATH to use toes globally"
|
||||
fi
|
||||
echo " Run ${c}toes${r} to get started."
|
||||
echo ""
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export type App = {
|
|||
tool?: boolean | string
|
||||
apps?: boolean
|
||||
dashboard?: boolean
|
||||
share?: boolean
|
||||
tunnelEnabled?: boolean
|
||||
tunnelUrl?: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,54 +11,36 @@ interface Listener {
|
|||
|
||||
const _listeners = new Set<Listener>()
|
||||
|
||||
let _abort: AbortController | undefined
|
||||
let _source: EventSource | undefined
|
||||
|
||||
function ensureConnection() {
|
||||
if (_abort) return
|
||||
if (_source) return
|
||||
const url = `${process.env.TOES_URL}/api/events/stream`
|
||||
_abort = new AbortController()
|
||||
connect(url, _abort.signal)
|
||||
}
|
||||
_source = new EventSource(url)
|
||||
|
||||
function closeConnection() {
|
||||
if (_abort) {
|
||||
_abort.abort()
|
||||
_abort = undefined
|
||||
_source.onerror = () => {
|
||||
if (_source?.readyState === EventSource.CLOSED) {
|
||||
console.warn('[toes] Event stream closed unexpectedly')
|
||||
_source = undefined
|
||||
}
|
||||
}
|
||||
|
||||
_source.onmessage = (e) => {
|
||||
try {
|
||||
const event: ToesEvent = JSON.parse(e.data)
|
||||
_listeners.forEach(l => {
|
||||
if (l.types.includes(event.type)) l.callback(event)
|
||||
})
|
||||
} catch {
|
||||
// Ignore malformed events
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function connect(url: string, signal: AbortSignal) {
|
||||
while (!signal.aborted) {
|
||||
try {
|
||||
const res = await fetch(url, { signal })
|
||||
if (!res.ok || !res.body) throw new Error(`SSE ${res.status}`)
|
||||
const reader = res.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buf = ''
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buf += decoder.decode(value, { stream: true })
|
||||
const parts = buf.split('\n\n')
|
||||
buf = parts.pop()!
|
||||
for (const part of parts) {
|
||||
const line = part.split('\n').find(l => l.startsWith('data:'))
|
||||
if (!line) continue
|
||||
try {
|
||||
const event: ToesEvent = JSON.parse(line.slice(5).trim())
|
||||
_listeners.forEach(l => {
|
||||
if (l.types.includes(event.type)) l.callback(event)
|
||||
})
|
||||
} catch {
|
||||
// Ignore malformed events
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (signal.aborted) return
|
||||
console.warn('[toes] Event stream error, reconnecting...')
|
||||
}
|
||||
if (!signal.aborted) await new Promise(r => setTimeout(r, 2000))
|
||||
function closeConnection() {
|
||||
if (_source) {
|
||||
_source.close()
|
||||
_source = undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user