@stravigor/create 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @stravigor/create
2
+
3
+ Scaffold a new [Strav](https://www.npmjs.com/package/@stravigor/core) application.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bunx @stravigor/create my-app
9
+ ```
10
+
11
+ ## Templates
12
+
13
+ - **api** — Headless REST API with CORS enabled
14
+ - **web** — Full-stack with `.strav` views, sessions, and static files
15
+
16
+ ## Options
17
+
18
+ ```
19
+ bunx @stravigor/create <project-name> [options]
20
+
21
+ --template, -t api|web Template to use (default: prompt)
22
+ --db <name> Database name (default: project name)
23
+ -h, --help Show help
24
+ ```
25
+
26
+ ## What's scaffolded
27
+
28
+ ```
29
+ my-app/
30
+ ├── index.ts Server entry point
31
+ ├── strav.ts CLI (migrations, generators)
32
+ ├── config/ Configuration files
33
+ ├── database/schemas/ Schema definitions
34
+ ├── start/routes.ts Route registration
35
+ ├── tests/ Test files
36
+ ├── .env Environment variables
37
+ └── package.json
38
+ ```
39
+
40
+ ## License
41
+
42
+ MIT
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@stravigor/create",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Scaffold a new Strav application",
6
+ "license": "MIT",
7
+ "keywords": ["strav", "stravigor", "bun", "framework", "scaffold", "create"],
8
+ "bin": {
9
+ "@stravigor/create": "./src/index.ts"
10
+ },
11
+ "files": ["src/", "package.json", "README.md"]
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync } from 'node:fs'
3
+ import { join, resolve } from 'node:path'
4
+ import { select, input } from './prompts.ts'
5
+ import { scaffold } from './scaffold.ts'
6
+ import type { ScaffoldOptions } from './templates/shared.ts'
7
+
8
+ const VERSION = '0.1.0'
9
+
10
+ // ── Colors ──────────────────────────────────────────────────────────
11
+
12
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`
13
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
14
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`
15
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`
16
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`
17
+
18
+ // ── Arg parsing ─────────────────────────────────────────────────────
19
+
20
+ interface ParsedArgs {
21
+ projectName?: string
22
+ template?: 'api' | 'web'
23
+ db?: string
24
+ help?: boolean
25
+ }
26
+
27
+ function parseArgs(): ParsedArgs {
28
+ const args = process.argv.slice(2)
29
+ const result: ParsedArgs = {}
30
+
31
+ for (let i = 0; i < args.length; i++) {
32
+ const arg = args[i]
33
+
34
+ if (arg === '--help' || arg === '-h') {
35
+ result.help = true
36
+ } else if (arg === '--template' || arg === '-t') {
37
+ const val = args[++i]
38
+ if (val === 'api' || val === 'web') {
39
+ result.template = val
40
+ } else {
41
+ console.error(red(` Invalid template: ${val}. Use "api" or "web".`))
42
+ process.exit(1)
43
+ }
44
+ } else if (arg === '--db') {
45
+ result.db = args[++i]
46
+ } else if (!arg.startsWith('-') && !result.projectName) {
47
+ result.projectName = arg
48
+ }
49
+ }
50
+
51
+ return result
52
+ }
53
+
54
+ function printUsage(): void {
55
+ console.log(`
56
+ ${bold('@stravigor/create')} ${dim(`v${VERSION}`)}
57
+
58
+ ${bold('Usage:')}
59
+ bunx @stravigor/create ${cyan('<project-name>')} [options]
60
+
61
+ ${bold('Options:')}
62
+ --template, -t ${dim('api|web')} Template to use (default: prompt)
63
+ --db ${dim('<name>')} Database name (default: project name)
64
+ -h, --help Show this help message
65
+ `)
66
+ }
67
+
68
+ function toSnakeCase(name: string): string {
69
+ return name.replace(/-/g, '_')
70
+ }
71
+
72
+ // ── Main ────────────────────────────────────────────────────────────
73
+
74
+ async function main(): Promise<void> {
75
+ const args = parseArgs()
76
+
77
+ if (args.help) {
78
+ printUsage()
79
+ process.exit(0)
80
+ }
81
+
82
+ console.log()
83
+ console.log(` ${bold('@stravigor/create')} ${dim(`v${VERSION}`)}`)
84
+ console.log()
85
+
86
+ // Project name
87
+ if (!args.projectName) {
88
+ printUsage()
89
+ process.exit(1)
90
+ }
91
+
92
+ const projectName = args.projectName
93
+ const root = resolve(projectName)
94
+
95
+ // Validate
96
+ if (existsSync(root)) {
97
+ console.error(red(` Directory "${projectName}" already exists.`))
98
+ process.exit(1)
99
+ }
100
+
101
+ if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
102
+ console.error(red(` Invalid project name. Use only letters, numbers, hyphens, and underscores.`))
103
+ process.exit(1)
104
+ }
105
+
106
+ // Template
107
+ let template = args.template
108
+ if (!template) {
109
+ template = await select('Which template?', [
110
+ { label: 'api', value: 'api', description: 'Headless REST API' },
111
+ { label: 'web', value: 'web', description: 'Full-stack with views and static files' },
112
+ ]) as 'api' | 'web'
113
+ }
114
+
115
+ // Database name
116
+ const defaultDb = toSnakeCase(projectName)
117
+ const dbName = args.db ?? await input('Database name:', defaultDb)
118
+
119
+ console.log()
120
+
121
+ // Scaffold
122
+ const opts: ScaffoldOptions = { projectName, template, dbName }
123
+ await scaffold(root, opts)
124
+ console.log(` ${green('+')} Scaffolded project files`)
125
+
126
+ // Install dependencies
127
+ console.log(` ${dim('...')} Installing dependencies`)
128
+ const install = Bun.spawn(['bun', 'install'], { cwd: root, stdout: 'ignore', stderr: 'pipe' })
129
+ const exitCode = await install.exited
130
+
131
+ if (exitCode !== 0) {
132
+ const stderr = await new Response(install.stderr).text()
133
+ console.error(red(` Failed to install dependencies:`))
134
+ console.error(dim(` ${stderr}`))
135
+ process.exit(1)
136
+ }
137
+
138
+ console.log(` ${green('+')} Installed dependencies`)
139
+
140
+ // Done
141
+ console.log()
142
+ console.log(` ${green('Project created successfully!')}`)
143
+ console.log()
144
+ console.log(` Next steps:`)
145
+ console.log()
146
+ console.log(` ${dim('$')} cd ${projectName}`)
147
+ console.log(` ${dim('$')} bun --hot index.ts`)
148
+ console.log()
149
+ console.log(` ${dim('Then open http://localhost:3000')}`)
150
+ console.log()
151
+ }
152
+
153
+ main().catch((err) => {
154
+ console.error(red(` Error: ${err instanceof Error ? err.message : err}`))
155
+ process.exit(1)
156
+ })
package/src/prompts.ts ADDED
@@ -0,0 +1,134 @@
1
+ const ESC = '\x1b'
2
+ const ARROW_UP = `${ESC}[A`
3
+ const ARROW_DOWN = `${ESC}[B`
4
+ const ENTER = '\r'
5
+
6
+ interface Choice {
7
+ label: string
8
+ value: string
9
+ description: string
10
+ }
11
+
12
+ export async function select(message: string, choices: Choice[]): Promise<string> {
13
+ let selected = 0
14
+
15
+ const render = () => {
16
+ // Move cursor up to overwrite previous render (except first time)
17
+ process.stdout.write(`\x1b[${choices.length}A`)
18
+ for (let i = 0; i < choices.length; i++) {
19
+ const prefix = i === selected ? '\x1b[36m>\x1b[0m' : ' '
20
+ const label = i === selected ? `\x1b[1m${choices[i].label}\x1b[0m` : choices[i].label
21
+ const desc = `\x1b[2m${choices[i].description}\x1b[0m`
22
+ process.stdout.write(`\x1b[2K ${prefix} ${label} ${desc}\n`)
23
+ }
24
+ }
25
+
26
+ process.stdout.write(` \x1b[1m${message}\x1b[0m\n`)
27
+ // Print initial lines so render() can overwrite them
28
+ for (const choice of choices) {
29
+ process.stdout.write('\n')
30
+ }
31
+ render()
32
+
33
+ return new Promise((resolve) => {
34
+ const stdin = process.stdin
35
+ stdin.setRawMode(true)
36
+ stdin.resume()
37
+ stdin.setEncoding('utf8')
38
+
39
+ let buffer = ''
40
+
41
+ const onData = (data: string) => {
42
+ buffer += data
43
+
44
+ // Check for Ctrl+C
45
+ if (buffer.includes('\x03')) {
46
+ stdin.setRawMode(false)
47
+ stdin.pause()
48
+ stdin.removeListener('data', onData)
49
+ process.stdout.write('\n')
50
+ process.exit(0)
51
+ }
52
+
53
+ // Process escape sequences
54
+ while (buffer.length > 0) {
55
+ if (buffer.startsWith(ARROW_UP)) {
56
+ selected = (selected - 1 + choices.length) % choices.length
57
+ render()
58
+ buffer = buffer.slice(ARROW_UP.length)
59
+ } else if (buffer.startsWith(ARROW_DOWN)) {
60
+ selected = (selected + 1) % choices.length
61
+ render()
62
+ buffer = buffer.slice(ARROW_DOWN.length)
63
+ } else if (buffer.startsWith(ENTER)) {
64
+ stdin.setRawMode(false)
65
+ stdin.pause()
66
+ stdin.removeListener('data', onData)
67
+ resolve(choices[selected].value)
68
+ buffer = ''
69
+ return
70
+ } else if (buffer.startsWith(ESC)) {
71
+ // Incomplete escape sequence, wait for more data
72
+ break
73
+ } else {
74
+ // Discard unrecognized input
75
+ buffer = buffer.slice(1)
76
+ }
77
+ }
78
+ }
79
+
80
+ stdin.on('data', onData)
81
+ })
82
+ }
83
+
84
+ export async function input(message: string, defaultValue: string): Promise<string> {
85
+ process.stdout.write(` \x1b[1m${message}\x1b[0m \x1b[2m(${defaultValue})\x1b[0m `)
86
+
87
+ return new Promise((resolve) => {
88
+ const stdin = process.stdin
89
+ stdin.setRawMode(true)
90
+ stdin.resume()
91
+ stdin.setEncoding('utf8')
92
+
93
+ let value = ''
94
+
95
+ const onData = (data: string) => {
96
+ for (const char of data) {
97
+ if (char === '\x03') {
98
+ // Ctrl+C
99
+ stdin.setRawMode(false)
100
+ stdin.pause()
101
+ stdin.removeListener('data', onData)
102
+ process.stdout.write('\n')
103
+ process.exit(0)
104
+ }
105
+
106
+ if (char === '\r' || char === '\n') {
107
+ stdin.setRawMode(false)
108
+ stdin.pause()
109
+ stdin.removeListener('data', onData)
110
+ process.stdout.write('\n')
111
+ resolve(value || defaultValue)
112
+ return
113
+ }
114
+
115
+ if (char === '\x7f' || char === '\b') {
116
+ // Backspace
117
+ if (value.length > 0) {
118
+ value = value.slice(0, -1)
119
+ process.stdout.write('\b \b')
120
+ }
121
+ continue
122
+ }
123
+
124
+ // Printable characters
125
+ if (char >= ' ') {
126
+ value += char
127
+ process.stdout.write(char)
128
+ }
129
+ }
130
+ }
131
+
132
+ stdin.on('data', onData)
133
+ })
134
+ }
@@ -0,0 +1,20 @@
1
+ import { mkdirSync } from 'node:fs'
2
+ import { join, dirname } from 'node:path'
3
+ import { getSharedFiles, type ScaffoldOptions } from './templates/shared.ts'
4
+ import { getApiFiles } from './templates/api.ts'
5
+ import { getWebFiles } from './templates/web.ts'
6
+
7
+ export async function scaffold(root: string, opts: ScaffoldOptions): Promise<void> {
8
+ // Collect all files
9
+ const files = [
10
+ ...getSharedFiles(opts),
11
+ ...(opts.template === 'web' ? getWebFiles(opts) : getApiFiles(opts)),
12
+ ]
13
+
14
+ // Create directories and write files
15
+ for (const file of files) {
16
+ const fullPath = join(root, file.path)
17
+ mkdirSync(dirname(fullPath), { recursive: true })
18
+ await Bun.write(fullPath, file.content)
19
+ }
20
+ }
@@ -0,0 +1,80 @@
1
+ import type { TemplateFile, ScaffoldOptions } from './shared.ts'
2
+
3
+ export function getApiFiles(opts: ScaffoldOptions): TemplateFile[] {
4
+ return [
5
+ { path: 'index.ts', content: indexTs(opts) },
6
+ { path: 'config/http.ts', content: configHttp() },
7
+ { path: 'start/routes.ts', content: routes(opts) },
8
+ ]
9
+ }
10
+
11
+ function indexTs(opts: ScaffoldOptions): string {
12
+ return `import 'reflect-metadata'
13
+ import { app } from '@stravigor/core/core'
14
+ import Configuration from '@stravigor/core/config/configuration'
15
+ import Database from '@stravigor/core/database/database'
16
+ import BaseModel from '@stravigor/core/orm/base_model'
17
+ import Router from '@stravigor/core/http/router'
18
+ import Server from '@stravigor/core/http/server'
19
+ import { ExceptionHandler } from '@stravigor/core/exceptions'
20
+ import EncryptionManager from '@stravigor/core/encryption/encryption_manager'
21
+
22
+ async function boot() {
23
+ const config = new Configuration('./config')
24
+ await config.load()
25
+ app.singleton(Configuration, () => config)
26
+
27
+ app.singleton(Database)
28
+ const db = app.resolve(Database)
29
+ new BaseModel(db)
30
+
31
+ app.singleton(EncryptionManager)
32
+ app.resolve(EncryptionManager)
33
+
34
+ app.singleton(Router)
35
+ const router = app.resolve(Router)
36
+
37
+ const handler = new ExceptionHandler(config.get('app.env') === 'local')
38
+ router.useExceptionHandler(handler)
39
+ router.cors()
40
+
41
+ await import('./start/routes')
42
+
43
+ app.singleton(Server)
44
+ const server = app.resolve(Server)
45
+ server.start(router)
46
+ }
47
+
48
+ boot().catch((err) => {
49
+ console.error('Failed to boot:', err)
50
+ process.exit(1)
51
+ })
52
+ `
53
+ }
54
+
55
+ function configHttp(): string {
56
+ return `import { env } from '@stravigor/core/helpers/env'
57
+
58
+ export default {
59
+ host: env('HOST', '0.0.0.0'),
60
+ port: env.int('PORT', 3000),
61
+ domain: env('DOMAIN', 'localhost'),
62
+ }
63
+ `
64
+ }
65
+
66
+ function routes(opts: ScaffoldOptions): string {
67
+ return `import { router } from '@stravigor/core/http'
68
+
69
+ router.get('/', () => {
70
+ return Response.json({
71
+ name: '${opts.projectName}',
72
+ status: 'running',
73
+ })
74
+ })
75
+
76
+ router.get('/health', () => {
77
+ return Response.json({ status: 'ok' })
78
+ })
79
+ `
80
+ }
@@ -0,0 +1,152 @@
1
+ export interface TemplateFile {
2
+ path: string
3
+ content: string
4
+ }
5
+
6
+ export interface ScaffoldOptions {
7
+ projectName: string
8
+ template: 'api' | 'web'
9
+ dbName: string
10
+ }
11
+
12
+ export function getSharedFiles(opts: ScaffoldOptions): TemplateFile[] {
13
+ const appKey = crypto.randomUUID()
14
+
15
+ return [
16
+ { path: 'package.json', content: packageJson(opts) },
17
+ { path: 'tsconfig.json', content: tsconfig() },
18
+ { path: '.env', content: dotEnv(opts, appKey) },
19
+ { path: '.gitignore', content: gitignore() },
20
+ { path: 'strav.ts', content: stravTs() },
21
+ { path: 'config/app.ts', content: configApp() },
22
+ { path: 'config/database.ts', content: configDatabase(opts) },
23
+ { path: 'config/encryption.ts', content: configEncryption() },
24
+ { path: 'database/schemas/.gitkeep', content: '' },
25
+ { path: 'tests/health.test.ts', content: healthTest(opts) },
26
+ ]
27
+ }
28
+
29
+ function packageJson(opts: ScaffoldOptions): string {
30
+ return JSON.stringify({
31
+ name: opts.projectName,
32
+ version: '0.0.1',
33
+ type: 'module',
34
+ private: true,
35
+ scripts: {
36
+ dev: 'bun --hot index.ts',
37
+ start: 'bun index.ts',
38
+ test: 'bun test tests/',
39
+ },
40
+ dependencies: {
41
+ '@stravigor/core': '^0.1.0',
42
+ 'luxon': '^3.7.2',
43
+ 'reflect-metadata': '^0.2.2',
44
+ },
45
+ devDependencies: {
46
+ '@types/bun': 'latest',
47
+ '@types/luxon': '^3.7.1',
48
+ '@stravigor/testing': '^0.1.0',
49
+ },
50
+ }, null, 2) + '\n'
51
+ }
52
+
53
+ function tsconfig(): string {
54
+ return JSON.stringify({
55
+ compilerOptions: {
56
+ lib: ['ESNext'],
57
+ target: 'ESNext',
58
+ module: 'ESNext',
59
+ moduleDetection: 'force',
60
+ allowJs: true,
61
+ moduleResolution: 'bundler',
62
+ allowImportingTsExtensions: true,
63
+ noEmit: true,
64
+ experimentalDecorators: true,
65
+ emitDecoratorMetadata: true,
66
+ strict: true,
67
+ skipLibCheck: true,
68
+ noFallthroughCasesInSwitch: true,
69
+ noUnusedLocals: false,
70
+ noUnusedParameters: false,
71
+ },
72
+ include: ['**/*.ts'],
73
+ }, null, 2) + '\n'
74
+ }
75
+
76
+ function dotEnv(opts: ScaffoldOptions, appKey: string): string {
77
+ return `APP_ENV=local
78
+ APP_DEBUG=true
79
+ APP_KEY=${appKey}
80
+
81
+ HOST=0.0.0.0
82
+ PORT=3000
83
+ DOMAIN=localhost
84
+
85
+ DB_HOST=127.0.0.1
86
+ DB_PORT=5432
87
+ DB_USERNAME=postgres
88
+ DB_PASSWORD=
89
+ DB_DATABASE=${opts.dbName}
90
+ `
91
+ }
92
+
93
+ function gitignore(): string {
94
+ return `node_modules/
95
+ .env
96
+ app/
97
+ database/migrations/
98
+ *.log
99
+ `
100
+ }
101
+
102
+ function stravTs(): string {
103
+ return `#!/usr/bin/env bun
104
+ import '@stravigor/core/cli/strav'
105
+ `
106
+ }
107
+
108
+ function configApp(): string {
109
+ return `import { env } from '@stravigor/core/helpers/env'
110
+
111
+ export default {
112
+ env: env('APP_ENV', 'local'),
113
+ debug: env.bool('APP_DEBUG', true),
114
+ key: env('APP_KEY'),
115
+ }
116
+ `
117
+ }
118
+
119
+ function configDatabase(opts: ScaffoldOptions): string {
120
+ return `import { env } from '@stravigor/core/helpers/env'
121
+
122
+ export default {
123
+ host: env('DB_HOST', '127.0.0.1'),
124
+ port: env.int('DB_PORT', 5432),
125
+ username: env('DB_USERNAME', 'postgres'),
126
+ password: env('DB_PASSWORD', ''),
127
+ database: env('DB_DATABASE', '${opts.dbName}'),
128
+ }
129
+ `
130
+ }
131
+
132
+ function configEncryption(): string {
133
+ return `import { env } from '@stravigor/core/helpers/env'
134
+
135
+ export default {
136
+ key: env('APP_KEY'),
137
+ previousKeys: [],
138
+ }
139
+ `
140
+ }
141
+
142
+ function healthTest(opts: ScaffoldOptions): string {
143
+ const path = opts.template === 'web' ? '/api/health' : '/health'
144
+ return `import { test, expect } from 'bun:test'
145
+
146
+ test('health check returns ok', async () => {
147
+ const res = await fetch('http://localhost:3000${path}')
148
+ const json = await res.json()
149
+ expect(json.status).toBe('ok')
150
+ })
151
+ `
152
+ }
@@ -0,0 +1,191 @@
1
+ import type { TemplateFile, ScaffoldOptions } from './shared.ts'
2
+
3
+ export function getWebFiles(opts: ScaffoldOptions): TemplateFile[] {
4
+ return [
5
+ { path: 'index.ts', content: indexTs(opts) },
6
+ { path: 'config/http.ts', content: configHttp() },
7
+ { path: 'config/session.ts', content: configSession() },
8
+ { path: 'config/view.ts', content: configView() },
9
+ { path: 'start/routes.ts', content: routes(opts) },
10
+ { path: 'views/welcome.strav', content: welcomeView() },
11
+ { path: 'public/styles.css', content: stylesCss() },
12
+ ]
13
+ }
14
+
15
+ function indexTs(opts: ScaffoldOptions): string {
16
+ return `import 'reflect-metadata'
17
+ import { app } from '@stravigor/core/core'
18
+ import Configuration from '@stravigor/core/config/configuration'
19
+ import Database from '@stravigor/core/database/database'
20
+ import BaseModel from '@stravigor/core/orm/base_model'
21
+ import Router from '@stravigor/core/http/router'
22
+ import Server from '@stravigor/core/http/server'
23
+ import { ExceptionHandler } from '@stravigor/core/exceptions'
24
+ import EncryptionManager from '@stravigor/core/encryption/encryption_manager'
25
+ import SessionManager from '@stravigor/core/session/session_manager'
26
+ import { ViewEngine } from '@stravigor/core/view'
27
+
28
+ async function boot() {
29
+ const config = new Configuration('./config')
30
+ await config.load()
31
+ app.singleton(Configuration, () => config)
32
+
33
+ app.singleton(Database)
34
+ const db = app.resolve(Database)
35
+ new BaseModel(db)
36
+
37
+ app.singleton(EncryptionManager)
38
+ app.resolve(EncryptionManager)
39
+
40
+ app.singleton(SessionManager)
41
+ app.resolve(SessionManager)
42
+
43
+ app.singleton(ViewEngine)
44
+ app.resolve(ViewEngine)
45
+
46
+ app.singleton(Router)
47
+ const router = app.resolve(Router)
48
+
49
+ const handler = new ExceptionHandler(config.get('app.env') === 'local')
50
+ router.useExceptionHandler(handler)
51
+
52
+ await import('./start/routes')
53
+
54
+ app.singleton(Server)
55
+ const server = app.resolve(Server)
56
+ server.start(router)
57
+ }
58
+
59
+ boot().catch((err) => {
60
+ console.error('Failed to boot:', err)
61
+ process.exit(1)
62
+ })
63
+ `
64
+ }
65
+
66
+ function configHttp(): string {
67
+ return `import { env } from '@stravigor/core/helpers/env'
68
+
69
+ export default {
70
+ host: env('HOST', '0.0.0.0'),
71
+ port: env.int('PORT', 3000),
72
+ domain: env('DOMAIN', 'localhost'),
73
+ public: './public',
74
+ }
75
+ `
76
+ }
77
+
78
+ function configSession(): string {
79
+ return `export default {
80
+ cookie: 'strav_session',
81
+ lifetime: 120,
82
+ httpOnly: true,
83
+ secure: false,
84
+ sameSite: 'lax' as const,
85
+ }
86
+ `
87
+ }
88
+
89
+ function configView(): string {
90
+ return `export default {
91
+ directory: 'views',
92
+ cache: false,
93
+ }
94
+ `
95
+ }
96
+
97
+ function routes(opts: ScaffoldOptions): string {
98
+ return `import { router } from '@stravigor/core/http'
99
+ import { view } from '@stravigor/core/view'
100
+
101
+ router.get('/', async () => {
102
+ return view('welcome', { name: '${opts.projectName}' })
103
+ })
104
+
105
+ router.get('/api/health', () => {
106
+ return Response.json({ status: 'ok' })
107
+ })
108
+ `
109
+ }
110
+
111
+ function welcomeView(): string {
112
+ return `<!DOCTYPE html>
113
+ <html lang="en">
114
+ <head>
115
+ <meta charset="UTF-8">
116
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
117
+ <title>{{ name }} — Strav</title>
118
+ <link rel="preconnect" href="https://fonts.googleapis.com">
119
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
120
+ <link href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@400;600;700&display=swap" rel="stylesheet">
121
+ <link rel="stylesheet" href="/styles.css">
122
+ </head>
123
+ <body>
124
+ <div class="container">
125
+ <h1>Welcome to Strav</h1>
126
+ <p>Your application <strong>{{ name }}</strong> is up and running.</p>
127
+ <div class="links">
128
+ <a href="https://github.com/nicoyambura/stravigor" target="_blank">Documentation</a>
129
+ </div>
130
+ </div>
131
+ </body>
132
+ </html>
133
+ `
134
+ }
135
+
136
+ function stylesCss(): string {
137
+ return `* {
138
+ margin: 0;
139
+ padding: 0;
140
+ box-sizing: border-box;
141
+ }
142
+
143
+ body {
144
+ font-family: 'Barlow Semi Condensed', sans-serif;
145
+ background: #f8fafc;
146
+ color: #1e293b;
147
+ min-height: 100vh;
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ }
152
+
153
+ .container {
154
+ text-align: center;
155
+ padding: 2rem;
156
+ }
157
+
158
+ h1 {
159
+ font-size: 2.5rem;
160
+ font-weight: 700;
161
+ margin-bottom: 1rem;
162
+ color: oklch(57.7% 0.245 27.325);
163
+ }
164
+
165
+ p {
166
+ font-size: 1.125rem;
167
+ color: #64748b;
168
+ margin-bottom: 2rem;
169
+ }
170
+
171
+ p strong {
172
+ color: #1e293b;
173
+ }
174
+
175
+ .links a {
176
+ color: #2563eb;
177
+ text-decoration: none;
178
+ font-size: 0.875rem;
179
+ font-weight: 600;
180
+ border: 1px solid #e2e8f0;
181
+ padding: 0.5rem 1rem;
182
+ border-radius: 0.375rem;
183
+ transition: all 0.2s;
184
+ }
185
+
186
+ .links a:hover {
187
+ background: #eff6ff;
188
+ border-color: #2563eb;
189
+ }
190
+ `
191
+ }