From baa3712fa2e938eaa8a7b8c39bced55323f8dad7 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 1 Mar 2026 14:57:39 -0800 Subject: [PATCH] Add getApp command and gitUrl helper --- src/cli/commands/index.ts | 1 + src/cli/commands/manage.ts | 27 +++++++++++++++++++++++++-- src/cli/http.ts | 3 +++ src/cli/setup.ts | 9 +++++++++ src/server/api/apps.ts | 5 ++++- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index 6c8785f..b4b3e7d 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -2,6 +2,7 @@ export { cronList, cronLog, cronRun, cronStatus } from './cron' export { envList, envRm, envSet } from './env' export { logApp } from './logs' export { + getApp, infoApp, listApps, newApp, diff --git a/src/cli/commands/manage.ts b/src/cli/commands/manage.ts index de7a966..d6720ab 100644 --- a/src/cli/commands/manage.ts +++ b/src/cli/commands/manage.ts @@ -4,7 +4,7 @@ import color from 'kleur' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' import { basename, join } from 'path' import { buildAppUrl } from '@urls' -import { del, get, getManifest, HOST, post } from '../http' +import { del, get, getManifest, gitUrl, HOST, post } from '../http' import { confirm, prompt } from '../prompts' import { resolveAppName } from '../name' @@ -179,7 +179,7 @@ export async function newApp(name: string | undefined, options: NewAppOptions) { await run(['git', 'init']) await run(['git', 'add', '.']) await run(['git', 'commit', '-m', 'init']) - await run(['git', 'remote', 'add', 'toes', `${HOST}/tool/git/${appName}`]) + await run(['git', 'remote', 'add', 'toes', gitUrl(appName)]) await run(['git', 'push', 'toes', 'main']) console.log(color.green(`✓ Created ${appName}`)) @@ -188,6 +188,29 @@ export async function newApp(name: string | undefined, options: NewAppOptions) { } } +export async function getApp(name: string, directory?: string) { + const target = directory ?? name + + if (existsSync(target)) { + console.error(`Directory already exists: ${target}`) + return + } + + const url = gitUrl(name) + const args = ['git', 'clone', url] + if (directory) args.push(directory) + const proc = Bun.spawn(args, { stdout: 'inherit', stderr: 'inherit' }) + const exitCode = await proc.exited + + if (exitCode !== 0) { + console.error(color.red(`Failed to clone ${name}`)) + return + } + + console.log(color.green(`✓ Cloned ${name}`)) + console.log(`\n cd ${target}\n bun install`) +} + export async function openApp(arg?: string) { const name = resolveAppName(arg) if (!name) return diff --git a/src/cli/http.ts b/src/cli/http.ts index 3801184..6a15200 100644 --- a/src/cli/http.ts +++ b/src/cli/http.ts @@ -1,4 +1,5 @@ import type { Manifest } from '@types' +import { buildAppUrl } from '@urls' import { AsyncLocalStorage } from 'node:async_hooks' const DEFAULT_HOST = process.env.DEV ? 'http://localhost:3000' : 'http://toes.local' @@ -20,6 +21,8 @@ export const HOST = process.env.TOES_URL ? normalizeUrl(process.env.TOES_URL) : DEFAULT_HOST +export const gitUrl = (name: string) => `${buildAppUrl('git', HOST)}/${name}` + export const getSignal = () => signalStore.getStore() export function withSignal(signal: AbortSignal, fn: () => T): T { diff --git a/src/cli/setup.ts b/src/cli/setup.ts index 5e03f02..e843a10 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -11,6 +11,7 @@ import { envList, envRm, envSet, + getApp, infoApp, listApps, logApp, @@ -75,6 +76,14 @@ program .option('--spa', 'single-page app with client-side rendering') .action(newApp) +program + .command('get') + .helpGroup('Apps:') + .description('Clone an app from the server') + .argument('', 'app name') + .argument('[directory]', 'target directory (defaults to app name)') + .action(getApp) + program .command('open') .helpGroup('Apps:') diff --git a/src/server/api/apps.ts b/src/server/api/apps.ts index d76bc90..8d90d98 100644 --- a/src/server/api/apps.ts +++ b/src/server/api/apps.ts @@ -1,4 +1,5 @@ import { APPS_DIR, TOES_DIR, TOES_URL, allApps, appendLog, getLogDates, onChange, readLogs, registerApp, renameApp, restartApp, startApp, stopApp, updateAppIcon } from '$apps' +import { buildAppUrl } from '@urls' import { isTunnelsAvailable, shareApp, unshareApp } from '../tunnels' import type { App as BackendApp } from '$apps' import type { App as SharedApp } from '@types' @@ -7,6 +8,8 @@ import { Hype } from '@because/hype' import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs' import { dirname, join } from 'path' +const gitUrl = (name: string) => `${buildAppUrl('git', TOES_URL)}/${name}` + const router = Hype.router() // BackendApp -> SharedApp @@ -143,7 +146,7 @@ router.post('/', async c => { await run(['git', 'init']) await run(['git', 'add', '.']) await run(['git', 'commit', '-m', 'init']) - await run(['git', 'remote', 'add', 'toes', `${TOES_URL}/tool/git/${name}`]) + await run(['git', 'remote', 'add', 'toes', gitUrl(name)]) const exitCode = await run(['git', 'push', 'toes', 'main']) if (exitCode !== 0) {