diff --git a/TODO.txt b/TODO.txt index 747de02..7e32dc9 100644 --- a/TODO.txt +++ b/TODO.txt @@ -28,7 +28,7 @@ [x] `toes logs ` [x] `toes logs -f ` [x] `toes info ` -[ ] `toes new` +[x] `toes new` [x] `toes pull` [x] `toes push` [ ] `toes sync` diff --git a/src/cli/index.ts b/src/cli/index.ts index 05c53f5..eaf442b 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun import type { App, LogLine } from '@types' import { loadGitignore } from '@gitignore' +import { generateTemplates } from '@templates' import { program } from 'commander' import { createHash } from 'crypto' import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'fs' @@ -156,7 +157,10 @@ function generateLocalManifest(appPath: string, appName: string): Manifest { } } -async function infoApp(name: string) { +async function infoApp(arg?: string) { + const name = resolveAppName(arg) + if (!name) return + const app: App | undefined = await get(`/api/apps/${name}`) if (!app) { console.error(`App not found: ${name}`) @@ -193,22 +197,31 @@ async function listApps() { } } -const startApp = async (app: string) => { - await post(`/api/apps/${app}/start`) +async function startApp(arg?: string) { + const name = resolveAppName(arg) + if (!name) return + await post(`/api/apps/${name}/start`) } -const stopApp = async (app: string) => { - await post(`/api/apps/${app}/stop`) +async function stopApp(arg?: string) { + const name = resolveAppName(arg) + if (!name) return + await post(`/api/apps/${name}/stop`) } -const restartApp = async (app: string) => { - await post(`/api/apps/${app}/restart`) +async function restartApp(arg?: string) { + const name = resolveAppName(arg) + if (!name) return + await post(`/api/apps/${name}/restart`) } const printLog = (line: LogLine) => console.log(`${new Date(line.time).toLocaleTimeString()} ${line.text}`) -async function logApp(name: string, options: { follow?: boolean }) { +async function logApp(arg: string | undefined, options: { follow?: boolean }) { + const name = resolveAppName(arg) + if (!name) return + if (options.follow) { await tailLogs(name) return @@ -258,7 +271,10 @@ async function tailLogs(name: string) { } } -async function openApp(name: string) { +async function openApp(arg?: string) { + const name = resolveAppName(arg) + if (!name) return + const app: App | undefined = await get(`/api/apps/${name}`) if (!app) { console.error(`App not found: ${name}`) @@ -322,6 +338,13 @@ function isApp(): boolean { } } +function resolveAppName(name?: string): string | undefined { + if (name) return name + if (isApp()) return basename(process.cwd()) + console.error('No app specified and current directory is not a toes app') + return undefined +} + async function pushApp() { if (!isApp()) { console.error('Not a toes app. Use `toes get ` to grab one.') @@ -347,8 +370,6 @@ async function pushApp() { if (!ok) return } - console.log(`Pushing ${color.bold(appName)} to server...`) - const localFiles = new Set(Object.keys(localManifest.files)) const remoteFiles = new Set(Object.keys(result.manifest?.files ?? {})) @@ -375,6 +396,8 @@ async function pushApp() { return } + console.log(`Pushing ${color.bold(appName)} to server...`) + if (toUpload.length > 0) { console.log(`Uploading ${toUpload.length} files...`) for (const file of toUpload) { @@ -436,77 +459,20 @@ async function newApp(name?: string) { const ok = await confirm(`Create ${color.bold(appName)} in ${appPath}?`) if (!ok) return - if (name) { - mkdirSync(appPath, { recursive: true }) + mkdirSync(join(appPath, 'src', 'pages'), { recursive: true }) + + const templates = generateTemplates(appName) + for (const [filename, content] of Object.entries(templates)) { + writeFileSync(join(appPath, filename), content) } - const packageJson = `{ - "name": "${appName}", - "module": "index.tsx", - "type": "module", - "private": true, - "scripts": { - "toes": "bun run --watch index.tsx", - "start": "bun toes", - "dev": "bun run --hot index.tsx" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5.9.2" - }, - "dependencies": { - "hype": "git+https://git.nose.space/defunkt/hype", - "forge": "git+https://git.nose.space/defunkt/forge" - } -} -` - - const indexTsx = `import { Hype } from 'hype' - -const app = new Hype() - -app.get('/', (c) => c.html(

${appName}

)) - -export default app.defaults -` - - const tsconfig = `{ - "compilerOptions": { - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", - "allowJs": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} -` - - writeFileSync(join(appPath, 'package.json'), packageJson) - writeFileSync(join(appPath, 'index.tsx'), indexTsx) - writeFileSync(join(appPath, 'tsconfig.json'), tsconfig) - console.log(color.green(`✓ Created ${appName}`)) console.log() console.log('Next steps:') if (name) { console.log(` cd ${name}`) } + console.log(' toes push') console.log(' bun install') console.log(' bun dev') } @@ -523,8 +489,6 @@ async function pullApp() { return } - console.log(`Pulling ${color.bold(appName)} from server...`) - const remoteManifest: Manifest | undefined = await get(`/api/sync/apps/${appName}/manifest`) if (!remoteManifest) { console.error('App not found on server') @@ -559,6 +523,8 @@ async function pullApp() { return } + console.log(`Pulling ${color.bold(appName)} from server...`) + if (toDownload.length > 0) { console.log(`Downloading ${toDownload.length} files...`) for (const file of toDownload) { @@ -599,7 +565,7 @@ program program .command('info') .description('Show info for an app') - .argument('', 'app name') + .argument('[name]', 'app name (uses current directory if omitted)') .action(infoApp) program @@ -610,38 +576,38 @@ program program .command('start') .description('Start an app') - .argument('', 'app name') + .argument('[name]', 'app name (uses current directory if omitted)') .action(startApp) program .command('stop') .description('Stop an app') - .argument('', 'app name') + .argument('[name]', 'app name (uses current directory if omitted)') .action(stopApp) program .command('restart') .description('Restart an app') - .argument('', 'app name') + .argument('[name]', 'app name (uses current directory if omitted)') .action(restartApp) program .command('logs') .description('Show logs for an app') - .argument('', 'app name') + .argument('[name]', 'app name (uses current directory if omitted)') .option('-f, --follow', 'follow log output') .action(logApp) program .command('log', { hidden: true }) - .argument('', 'app name') + .argument('[name]', 'app name (uses current directory if omitted)') .option('-f, --follow', 'follow log output') .action(logApp) program .command('open') .description('Open an app in browser') - .argument('', 'app name') + .argument('[name]', 'app name (uses current directory if omitted)') .action(openApp) program diff --git a/src/shared/templates.ts b/src/shared/templates.ts new file mode 100644 index 0000000..f0e70a5 --- /dev/null +++ b/src/shared/templates.ts @@ -0,0 +1,64 @@ +export interface AppTemplates { + 'index.tsx': string + 'package.json': string + 'tsconfig.json': string + '.npmrc': string + 'src/pages/index.tsx': string +} + +const tsconfig = { + compilerOptions: { + lib: ['ESNext'], + target: 'ESNext', + module: 'Preserve', + moduleDetection: 'force', + jsx: 'react-jsx', + jsxImportSource: 'hono/jsx', + allowJs: true, + moduleResolution: 'bundler', + allowImportingTsExtensions: true, + verbatimModuleSyntax: true, + noEmit: true, + strict: true, + skipLibCheck: true, + noFallthroughCasesInSwitch: true, + noUncheckedIndexedAccess: true, + noImplicitOverride: true, + noUnusedLocals: false, + noUnusedParameters: false, + noPropertyAccessFromIndexSignature: false, + }, +} + +export function generateTemplates(appName: string): AppTemplates { + const packageJson = { + name: appName, + module: 'index.tsx', + type: 'module', + private: true, + scripts: { + toes: 'bun run --watch index.tsx', + start: 'bun toes', + dev: 'bun run --hot index.tsx', + }, + devDependencies: { + '@types/bun': 'latest', + }, + peerDependencies: { + typescript: '^5.9.2', + }, + dependencies: { + '@because/hype': '*', + '@because/forge': '*', + '@because/howl': '*', + }, + } + + return { + '.npmrc': 'registry=https://npm.nose.space', + 'package.json': JSON.stringify(packageJson, null, 2) + '\n', + 'src/pages/index.tsx': `export default () =>

${appName}

`, + 'index.tsx': `import { Hype } from '@because/hype'\nconst app = new Hype\nexport default app.defaults`, + 'tsconfig.json': JSON.stringify(tsconfig, null, 2) + '\n', + } +}