create-top-secret-starter 0.1.0 → 0.2.1
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/index.js +243 -39
- package/package.json +2 -2
- package/templates/template-api/.env.example +13 -0
- package/templates/template-api/CLAUDE.md +32 -0
- package/templates/template-api/drizzle/0000_damp_taskmaster.sql +64 -0
- package/templates/template-api/drizzle/meta/0000_snapshot.json +469 -0
- package/templates/template-api/drizzle/meta/_journal.json +13 -0
- package/templates/template-api/drizzle.config.ts +13 -0
- package/templates/template-api/package.json +37 -0
- package/templates/template-api/src/app.test.ts +119 -0
- package/templates/template-api/src/app.ts +26 -0
- package/templates/template-api/src/db/auth-schema.ts +92 -0
- package/templates/template-api/src/db/index.ts +12 -0
- package/templates/template-api/src/db/migrate.ts +11 -0
- package/templates/template-api/src/db/schema.ts +21 -0
- package/templates/template-api/src/env.ts +30 -0
- package/templates/template-api/src/index.ts +38 -0
- package/templates/template-api/src/modules/auth/index.ts +6 -0
- package/templates/template-api/src/modules/auth/service.ts +53 -0
- package/templates/template-api/src/modules/notes/index.ts +38 -0
- package/templates/template-api/src/modules/notes/model.ts +23 -0
- package/templates/template-api/src/modules/notes/service.ts +36 -0
- package/templates/template-api/src/redis.ts +21 -0
- package/templates/template-api/tsconfig.json +23 -0
- package/templates/template-api/vitest.config.ts +11 -0
- package/templates/template-router/.claude/hooks/stop-check.sh +26 -0
- package/templates/template-router/.claude/launch.json +11 -0
- package/templates/template-router/.claude/settings.json +38 -0
- package/templates/template-router/.claude/skills/code-review/SKILL.md +89 -0
- package/templates/template-router/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/templates/template-router/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/templates/template-router/.claude/skills/codebase-design/SKILL.md +114 -0
- package/templates/template-router/.claude/skills/diagnosing-bugs/SKILL.md +134 -0
- package/templates/template-router/.claude/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/templates/template-router/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +129 -0
- package/templates/template-router/.claude/skills/improve-codebase-architecture/SKILL.md +66 -0
- package/templates/template-router/.claude/skills/project-conventions/SKILL.md +140 -0
- package/templates/template-router/.claude/skills/react-doctor/AGENTS.md +17 -0
- package/templates/template-router/.claude/skills/react-doctor/SKILL.md +21 -0
- package/templates/template-router/.mcp.json +12 -0
- package/templates/template-router/.oxfmtrc.json +4 -2
- package/templates/template-router/.oxlintrc.json +47 -3
- package/templates/template-router/CLAUDE.md +59 -0
- package/templates/template-router/_gitignore +3 -0
- package/templates/template-router/package.json +10 -12
- package/templates/template-router/src/api/auth/guards.ts +1 -4
- package/templates/template-router/src/api/auth/index.ts +7 -21
- package/templates/template-router/src/api/auth/refresh.test.ts +6 -12
- package/templates/template-router/src/api/auth/router-bridge.ts +7 -21
- package/templates/template-router/src/api/auth/session-store.test.ts +11 -12
- package/templates/template-router/src/api/auth/session-store.ts +1 -6
- package/templates/template-router/src/api/index.ts +4 -3
- package/templates/template-router/src/components/status-page.tsx +1 -4
- package/templates/template-router/src/components/ui/field.tsx +18 -23
- package/templates/template-router/src/components/ui/sonner.tsx +2 -2
- package/templates/template-router/src/env.ts +1 -2
- package/templates/template-router/src/index.css +30 -3
- package/templates/template-router/src/lib/query-client.test.ts +6 -8
- package/templates/template-router/src/lib/query-client.ts +4 -3
- package/templates/template-router/src/lib/utils.ts +1 -1
- package/templates/template-router/src/main.tsx +1 -4
- package/templates/template-router/src/mocks/mock-server.ts +10 -21
- package/templates/template-router/src/providers/index.tsx +1 -2
- package/templates/template-router/src/providers/theme.test.tsx +7 -4
- package/templates/template-router/src/providers/theme.tsx +38 -30
- package/templates/template-router/src/router.tsx +7 -7
- package/templates/template-router/src/routes/-error.tsx +1 -1
- package/templates/template-router/src/routes/-pending.tsx +12 -0
- package/templates/template-router/src/routes/__root.tsx +1 -5
- package/templates/template-router/src/routes/_authenticated/index.tsx +1 -3
- package/templates/template-router/src/routes/sign-in.tsx +7 -12
- package/templates/template-router/tsconfig.app.json +2 -1
- package/templates/template-router/tsconfig.json +11 -2
- package/templates/template-router/tsconfig.node.json +1 -0
- package/templates/template-router/vite.config.ts +18 -11
- package/templates/template-start/.claude/hooks/stop-check.sh +26 -0
- package/templates/template-start/.claude/launch.json +11 -0
- package/templates/template-start/.claude/settings.json +38 -0
- package/templates/template-start/.claude/skills/code-review/SKILL.md +89 -0
- package/templates/template-start/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/templates/template-start/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/templates/template-start/.claude/skills/codebase-design/SKILL.md +114 -0
- package/templates/template-start/.claude/skills/diagnosing-bugs/SKILL.md +134 -0
- package/templates/template-start/.claude/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/templates/template-start/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +129 -0
- package/templates/template-start/.claude/skills/improve-codebase-architecture/SKILL.md +66 -0
- package/templates/template-start/.claude/skills/project-conventions/SKILL.md +140 -0
- package/templates/template-start/.claude/skills/react-doctor/AGENTS.md +17 -0
- package/templates/template-start/.claude/skills/react-doctor/SKILL.md +21 -0
- package/templates/template-start/.mcp.json +12 -0
- package/templates/template-start/.oxfmtrc.json +4 -2
- package/templates/template-start/.oxlintrc.json +47 -3
- package/templates/template-start/CLAUDE.md +59 -0
- package/templates/template-start/_gitignore +3 -0
- package/templates/template-start/package.json +10 -12
- package/templates/template-start/src/api/auth/guards.ts +9 -4
- package/templates/template-start/src/api/auth/index.ts +7 -21
- package/templates/template-start/src/api/auth/refresh.test.ts +6 -12
- package/templates/template-start/src/api/auth/router-bridge.ts +7 -21
- package/templates/template-start/src/api/auth/session-store.test.ts +11 -12
- package/templates/template-start/src/api/auth/session-store.ts +19 -13
- package/templates/template-start/src/api/index.ts +4 -3
- package/templates/template-start/src/components/status-page.tsx +1 -4
- package/templates/template-start/src/components/ui/field.tsx +18 -23
- package/templates/template-start/src/components/ui/sonner.tsx +2 -2
- package/templates/template-start/src/env.ts +1 -2
- package/templates/template-start/src/index.css +30 -3
- package/templates/template-start/src/lib/query-client.test.ts +6 -8
- package/templates/template-start/src/lib/query-client.ts +4 -3
- package/templates/template-start/src/lib/utils.ts +1 -1
- package/templates/template-start/src/mocks/mock-server.ts +10 -21
- package/templates/template-start/src/providers/index.tsx +1 -2
- package/templates/template-start/src/providers/theme.test.tsx +7 -4
- package/templates/template-start/src/providers/theme.tsx +38 -30
- package/templates/template-start/src/router.tsx +8 -8
- package/templates/template-start/src/routes/-error.tsx +1 -1
- package/templates/template-start/src/routes/-pending.tsx +12 -0
- package/templates/template-start/src/routes/__root.tsx +2 -1
- package/templates/template-start/src/routes/_authenticated/index.tsx +1 -3
- package/templates/template-start/src/routes/sign-in.tsx +7 -12
- package/templates/template-start/tsconfig.app.json +2 -1
- package/templates/template-start/tsconfig.json +11 -2
- package/templates/template-start/tsconfig.node.json +1 -0
- package/templates/template-router/src/api/auth/router-bridge.test.ts +0 -74
- package/templates/template-start/src/api/auth/router-bridge.test.ts +0 -74
package/index.js
CHANGED
|
@@ -5,21 +5,28 @@ import { fileURLToPath } from 'node:url'
|
|
|
5
5
|
import { parseArgs } from 'node:util'
|
|
6
6
|
import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts'
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// the package manager that launched us sets npm_config_user_agent
|
|
9
9
|
const agent = process.env.npm_config_user_agent ?? ''
|
|
10
10
|
const detectedPm = agent.split('/')[0] || 'npm'
|
|
11
11
|
|
|
12
12
|
const PMS = ['bun', 'pnpm', 'npm']
|
|
13
|
-
const
|
|
13
|
+
const ROUTER_LABELS = { router: 'TanStack Router', start: 'TanStack Start' }
|
|
14
|
+
const ROUTERS = Object.keys(ROUTER_LABELS)
|
|
14
15
|
const NAME_RE = /^[a-z0-9][a-z0-9._-]*$/
|
|
16
|
+
// web-side runtime deps the backend variant adds (the api types come in as a devDep)
|
|
17
|
+
const BACKEND_WEB_DEPS = { '@elysia/eden': '^1.4.10', 'better-auth': '^1.7.2' }
|
|
15
18
|
|
|
16
|
-
//
|
|
19
|
+
// flags bypass prompts so the CLI works in CI
|
|
17
20
|
const { values: flags, positionals } = parseArgs({
|
|
18
21
|
args: process.argv.slice(2),
|
|
19
22
|
allowPositionals: true,
|
|
20
23
|
options: {
|
|
21
24
|
pm: { type: 'string' },
|
|
22
|
-
router: { type: 'string' }
|
|
25
|
+
router: { type: 'string' },
|
|
26
|
+
backend: { type: 'boolean' },
|
|
27
|
+
'no-backend': { type: 'boolean' },
|
|
28
|
+
redis: { type: 'boolean' },
|
|
29
|
+
'no-redis': { type: 'boolean' }
|
|
23
30
|
}
|
|
24
31
|
})
|
|
25
32
|
|
|
@@ -38,18 +45,24 @@ function validateName(v) {
|
|
|
38
45
|
|
|
39
46
|
intro('create-top-secret-starter')
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
// `.` / `./` scaffold into the current directory; the package name is then the
|
|
49
|
+
// directory's basename
|
|
50
|
+
const target =
|
|
51
|
+
positionals.at(0) ??
|
|
52
|
+
must(
|
|
53
|
+
await text({
|
|
54
|
+
message: 'Project name (or . for the current directory)?',
|
|
55
|
+
placeholder: 'my-ai-app',
|
|
56
|
+
validate: v => (v ? undefined : 'Project name is required')
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
const targetDir = path.resolve(process.cwd(), target)
|
|
60
|
+
const inPlace = targetDir === process.cwd()
|
|
61
|
+
const name = path.basename(targetDir)
|
|
62
|
+
if (validateName(name)) {
|
|
43
63
|
cancel(`Invalid project name "${name}": ${validateName(name)}`)
|
|
44
64
|
process.exit(1)
|
|
45
65
|
}
|
|
46
|
-
name ??= must(
|
|
47
|
-
await text({
|
|
48
|
-
message: 'Project name?',
|
|
49
|
-
placeholder: 'my-ai-app',
|
|
50
|
-
validate: validateName
|
|
51
|
-
})
|
|
52
|
-
)
|
|
53
66
|
|
|
54
67
|
let pm = flags.pm
|
|
55
68
|
if (pm && !PMS.includes(pm)) {
|
|
@@ -73,45 +86,236 @@ router ??= must(
|
|
|
73
86
|
await select({
|
|
74
87
|
message: 'Routing?',
|
|
75
88
|
options: [
|
|
76
|
-
{ value: 'router', label:
|
|
77
|
-
{ value: 'start', label:
|
|
89
|
+
{ value: 'router', label: ROUTER_LABELS.router, hint: 'SPA' },
|
|
90
|
+
{ value: 'start', label: ROUTER_LABELS.start, hint: 'SSR, full-stack' }
|
|
78
91
|
]
|
|
79
92
|
})
|
|
80
93
|
)
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
// Elysia runs on Bun, so the backend is only offered when bun is the package manager.
|
|
96
|
+
const flagPair = (on, off) => {
|
|
97
|
+
if (on) return true
|
|
98
|
+
if (off) return false
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let backend = flagPair(flags.backend, flags['no-backend'])
|
|
103
|
+
if (backend && pm !== 'bun') {
|
|
104
|
+
cancel('The backend (Elysia) requires bun: pass --pm bun.')
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
if (backend === undefined && pm === 'bun') {
|
|
108
|
+
backend = must(
|
|
109
|
+
await select({
|
|
110
|
+
message: 'Backend?',
|
|
111
|
+
options: [
|
|
112
|
+
{ value: false, label: 'None', hint: 'SPA only, mock API in dev' },
|
|
113
|
+
{ value: true, label: 'Elysia + Drizzle + Better Auth', hint: 'Postgres, bun workspace' }
|
|
114
|
+
]
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
backend ??= false
|
|
119
|
+
|
|
120
|
+
// Redis (Bun's built-in client) as Better Auth's secondary storage: sessions and
|
|
121
|
+
// rate-limit counters survive restarts and are shared across instances.
|
|
122
|
+
let redis = flagPair(flags.redis, flags['no-redis'])
|
|
123
|
+
if (redis && !backend) {
|
|
124
|
+
cancel('--redis requires the backend: pass --backend.')
|
|
85
125
|
process.exit(1)
|
|
86
126
|
}
|
|
127
|
+
if (redis === undefined && backend) {
|
|
128
|
+
redis = must(
|
|
129
|
+
await select({
|
|
130
|
+
message: 'Redis?',
|
|
131
|
+
options: [
|
|
132
|
+
{ value: false, label: 'None', hint: 'sessions in Postgres, rate limit per process' },
|
|
133
|
+
{ value: true, label: 'Redis', hint: 'Better Auth secondary storage, bun.redis' }
|
|
134
|
+
]
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
redis ??= false
|
|
87
139
|
|
|
88
|
-
//
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
140
|
+
// a fresh `git init` is fine, anything else would be overwritten
|
|
141
|
+
const existing = fs.existsSync(targetDir)
|
|
142
|
+
? fs.readdirSync(targetDir).filter(entry => entry !== '.git')
|
|
143
|
+
: []
|
|
144
|
+
if (existing.length > 0) {
|
|
145
|
+
cancel(`Directory "${target}" is not empty.`)
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// one full template per router choice
|
|
150
|
+
const cliRoot = path.dirname(fileURLToPath(import.meta.url))
|
|
151
|
+
const templateDir = path.join(cliRoot, 'templates', `template-${router}`)
|
|
94
152
|
if (!fs.existsSync(templateDir)) {
|
|
95
|
-
cancel(`The ${router
|
|
153
|
+
cancel(`The ${ROUTER_LABELS[router]} template is not available yet.`)
|
|
96
154
|
process.exit(1)
|
|
97
155
|
}
|
|
98
|
-
|
|
156
|
+
// npm strips .gitignore from packages, so it ships as _gitignore
|
|
157
|
+
const restoreGitignore = dir => {
|
|
158
|
+
const src = path.join(dir, '_gitignore')
|
|
159
|
+
if (fs.existsSync(src)) fs.renameSync(src, path.join(dir, '.gitignore'))
|
|
160
|
+
}
|
|
161
|
+
const patchFile = (file, fn) => fs.writeFileSync(file, fn(fs.readFileSync(file, 'utf8')))
|
|
162
|
+
// The api source keeps Redis-only code between `redis:start` / `redis:end` marker
|
|
163
|
+
// comments, so one source tree serves both answers to the Redis prompt.
|
|
164
|
+
const REDIS_MARKER = /^[ \t]*(\/\/|#|<!--) redis:(start|end)( -->)?\n/gm
|
|
165
|
+
const REDIS_BLOCK =
|
|
166
|
+
/^[ \t]*(\/\/|#|<!--) redis:start( -->)?\n[\s\S]*?^[ \t]*(\/\/|#|<!--) redis:end( -->)?\n/gm
|
|
167
|
+
// each removal site is marked so the comma repair below stays local to it
|
|
168
|
+
const HOLE = '__REDIS_HOLE__'
|
|
169
|
+
|
|
170
|
+
const applyRedisMarkers = (file, keep) =>
|
|
171
|
+
patchFile(file, source =>
|
|
172
|
+
source
|
|
173
|
+
.replace(keep ? REDIS_MARKER : REDIS_BLOCK, HOLE)
|
|
174
|
+
// dropping the last entry of an object or call leaves a dangling comma
|
|
175
|
+
.replace(new RegExp(`,(\\s*)${HOLE}(\\s*[)}\\]])`, 'g'), '$1$2')
|
|
176
|
+
.replaceAll(HOLE, '')
|
|
177
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
// walk rather than a hardcoded list: marked code may live in any api file
|
|
181
|
+
const filesWithRedisMarkers = dir =>
|
|
182
|
+
fs
|
|
183
|
+
.readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
184
|
+
.filter(entry => entry.isFile())
|
|
185
|
+
.map(entry => path.join(entry.parentPath, entry.name))
|
|
186
|
+
.filter(file => fs.readFileSync(file, 'utf8').includes('redis:start'))
|
|
187
|
+
|
|
188
|
+
const editJson = (file, fn) => {
|
|
189
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
190
|
+
fn(value)
|
|
191
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const webDir = backend ? path.join(targetDir, 'apps', 'web') : targetDir
|
|
195
|
+
fs.cpSync(templateDir, webDir, { recursive: true })
|
|
196
|
+
|
|
197
|
+
// The backend variant is the flat template plus an overlay: files the Eden/Better
|
|
198
|
+
// Auth stack replaces are deleted, the rest is copied over, and the workspace root
|
|
199
|
+
// is laid down around it.
|
|
200
|
+
const applyBackendOverlay = () => {
|
|
201
|
+
const overlay = path.join(cliRoot, 'backend-overlay')
|
|
202
|
+
|
|
203
|
+
for (const file of JSON.parse(fs.readFileSync(path.join(overlay, 'remove.json'), 'utf8'))) {
|
|
204
|
+
fs.rmSync(path.join(webDir, file), { recursive: true, force: true })
|
|
205
|
+
}
|
|
206
|
+
fs.cpSync(path.join(overlay, 'web'), webDir, { recursive: true })
|
|
207
|
+
fs.cpSync(path.join(overlay, `web-${router}`), webDir, { recursive: true })
|
|
208
|
+
// one .gitignore, at the workspace root
|
|
209
|
+
fs.rmSync(path.join(webDir, '_gitignore'), { force: true })
|
|
210
|
+
|
|
211
|
+
// dev proxy: same origin for the session cookie
|
|
212
|
+
patchFile(path.join(webDir, 'vite.config.ts'), source =>
|
|
213
|
+
source.replace(
|
|
214
|
+
'export default defineConfig({\n',
|
|
215
|
+
"export default defineConfig({\n server: { proxy: { '/api': 'http://localhost:3000' } },\n"
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const templatePkg = JSON.parse(fs.readFileSync(path.join(webDir, 'package.json'), 'utf8'))
|
|
220
|
+
editJson(path.join(webDir, 'package.json'), pkg => {
|
|
221
|
+
pkg.name = '@repo/web'
|
|
222
|
+
delete pkg.dependencies.ky
|
|
223
|
+
Object.assign(pkg.dependencies, BACKEND_WEB_DEPS)
|
|
224
|
+
pkg.dependencies = Object.fromEntries(
|
|
225
|
+
Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b))
|
|
226
|
+
)
|
|
227
|
+
// type-only import (Eden); as a runtime dep it would drag elysia/drizzle into
|
|
228
|
+
// the web app's production dependency graph
|
|
229
|
+
pkg.devDependencies['@repo/api'] = 'workspace:*'
|
|
230
|
+
pkg.devDependencies = Object.fromEntries(
|
|
231
|
+
Object.entries(pkg.devDependencies).sort(([a], [b]) => a.localeCompare(b))
|
|
232
|
+
)
|
|
233
|
+
// lint and format run once, from the workspace root
|
|
234
|
+
for (const script of ['lint', 'lint:fix', 'format', 'format:check']) delete pkg.scripts[script]
|
|
235
|
+
delete pkg.trustedDependencies
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const apiDir = path.join(targetDir, 'apps', 'api')
|
|
239
|
+
fs.cpSync(path.join(cliRoot, 'templates', 'template-api'), apiDir, { recursive: true })
|
|
240
|
+
if (!redis) fs.rmSync(path.join(apiDir, 'src', 'redis.ts'))
|
|
241
|
+
for (const file of filesWithRedisMarkers(apiDir)) applyRedisMarkers(file, redis)
|
|
242
|
+
// an unbalanced marker would otherwise ship Redis code into a --no-redis project
|
|
243
|
+
const leftover = filesWithRedisMarkers(apiDir)
|
|
244
|
+
if (leftover.length > 0) {
|
|
245
|
+
cancel(`Unbalanced redis: markers in ${leftover.join(', ')} - this is a template bug.`)
|
|
246
|
+
process.exit(1)
|
|
247
|
+
}
|
|
248
|
+
fs.cpSync(path.join(overlay, 'root'), targetDir, { recursive: true })
|
|
249
|
+
restoreGitignore(targetDir)
|
|
250
|
+
|
|
251
|
+
// Claude Code config belongs to the workspace root, not the web app
|
|
252
|
+
for (const file of ['.claude', '.mcp.json', 'CLAUDE.md']) {
|
|
253
|
+
fs.renameSync(path.join(webDir, file), path.join(targetDir, file))
|
|
254
|
+
}
|
|
255
|
+
patchFile(
|
|
256
|
+
path.join(targetDir, 'CLAUDE.md'),
|
|
257
|
+
source =>
|
|
258
|
+
source
|
|
259
|
+
.replace('`@/` → `src/`.', '`@/` → `apps/web/src/`.')
|
|
260
|
+
.replaceAll('`src/', '`apps/web/src/') +
|
|
261
|
+
'\n## Backend\n\n`apps/api` (Elysia + Drizzle + Better Auth) has its own `CLAUDE.md`. The web app talks to it through the Eden client; the Vite dev proxy keeps the session cookie first-party.\n'
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
for (const file of ['.oxfmtrc.json', '.oxlintrc.json']) {
|
|
265
|
+
const dest = path.join(targetDir, file)
|
|
266
|
+
fs.renameSync(path.join(webDir, file), dest)
|
|
267
|
+
// textual edit: re-serialising would break the oxfmt style the file itself enforces
|
|
268
|
+
patchFile(dest, source => source.replaceAll('"src/', '"apps/web/src/'))
|
|
269
|
+
}
|
|
270
|
+
editJson(path.join(targetDir, 'package.json'), pkg => {
|
|
271
|
+
pkg.name = name
|
|
272
|
+
pkg.devDependencies.oxfmt = templatePkg.devDependencies.oxfmt
|
|
273
|
+
pkg.devDependencies.oxlint = templatePkg.devDependencies.oxlint
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
for (const file of [
|
|
277
|
+
'README.md',
|
|
278
|
+
path.join('apps', 'web', 'src', 'routes', '_authenticated', 'index.tsx')
|
|
279
|
+
]) {
|
|
280
|
+
patchFile(path.join(targetDir, file), source => source.replaceAll('PLACEHOLDER', name))
|
|
281
|
+
}
|
|
282
|
+
}
|
|
99
283
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
284
|
+
if (backend) {
|
|
285
|
+
applyBackendOverlay()
|
|
286
|
+
} else {
|
|
287
|
+
restoreGitignore(targetDir)
|
|
288
|
+
editJson(path.join(targetDir, 'package.json'), pkg => {
|
|
289
|
+
pkg.name = name
|
|
290
|
+
})
|
|
291
|
+
// textual edits: re-serialising would break the oxfmt style the project enforces
|
|
292
|
+
if (pm !== 'bun') {
|
|
293
|
+
patchFile(path.join(targetDir, '.claude', 'launch.json'), source =>
|
|
294
|
+
source.replace('"runtimeExecutable": "bun"', `"runtimeExecutable": "${pm}"`)
|
|
295
|
+
)
|
|
296
|
+
// MCP servers start through the pm's package runner
|
|
297
|
+
patchFile(path.join(targetDir, '.mcp.json'), source =>
|
|
298
|
+
pm === 'pnpm'
|
|
299
|
+
? source
|
|
300
|
+
.replaceAll('"command": "bunx"', '"command": "pnpm"')
|
|
301
|
+
.replaceAll('"args": [', '"args": ["dlx", ')
|
|
302
|
+
: source.replaceAll('"command": "bunx"', '"command": "npx"')
|
|
303
|
+
)
|
|
304
|
+
}
|
|
105
305
|
}
|
|
106
306
|
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
307
|
+
const label = `${ROUTER_LABELS[router]}${backend ? ' + Elysia/Drizzle/Better Auth' : ''}${redis ? ' + Redis' : ''}`
|
|
308
|
+
const steps = backend
|
|
309
|
+
? [
|
|
310
|
+
...(inPlace ? [] : [`cd ${target}`]),
|
|
311
|
+
'bun install',
|
|
312
|
+
`cp apps/api/.env.example apps/api/.env # set DATABASE_URL${redis ? ', REDIS_URL' : ''}, BETTER_AUTH_SECRET`,
|
|
313
|
+
'bun run db:migrate',
|
|
314
|
+
'bun run dev'
|
|
315
|
+
]
|
|
316
|
+
: [...(inPlace ? [] : [`cd ${target}`]), `${pm} install`, `${pm} run dev`]
|
|
111
317
|
|
|
112
|
-
outro(`✔ ${name} created (${
|
|
318
|
+
outro(`✔ ${name} created (${label})
|
|
113
319
|
|
|
114
320
|
Next steps:
|
|
115
|
-
|
|
116
|
-
${pm} install
|
|
117
|
-
${pm} run dev`)
|
|
321
|
+
${steps.map(s => ` ${s}`).join('\n')}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-top-secret-starter",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Scaffold a Vite + React 19 + TypeScript + shadcn starter with TanStack Router",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"create",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"@clack/prompts": "^1.7.0"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=20"
|
|
31
|
+
"node": ">=20.12"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
DATABASE_URL=postgres://user:password@host:5432/dbname
|
|
2
|
+
# openssl rand -base64 32
|
|
3
|
+
BETTER_AUTH_SECRET=
|
|
4
|
+
# public origin the browser talks to — the Vite dev server when proxying /api;
|
|
5
|
+
# https in production (also enables secure cookies)
|
|
6
|
+
BETTER_AUTH_URL=http://localhost:5173
|
|
7
|
+
PORT=3000
|
|
8
|
+
# comma-separated extra origins; leave empty when the SPA is served from the same origin
|
|
9
|
+
CORS_ORIGINS=
|
|
10
|
+
# redis:start
|
|
11
|
+
# optional: sessions + rate-limit counters in Redis instead of Postgres/memory
|
|
12
|
+
REDIS_URL=redis://localhost:6379
|
|
13
|
+
# redis:end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# apps/api
|
|
2
|
+
|
|
3
|
+
Elysia + Drizzle (Postgres via postgres.js) + Better Auth. Bun workspace member;
|
|
4
|
+
root `verify` covers it. Tests run on PGlite with the committed migrations — no
|
|
5
|
+
server needed.
|
|
6
|
+
|
|
7
|
+
- Feature layout per [Elysia best practices](https://elysiajs.com/essential/best-practice):
|
|
8
|
+
`src/modules/<feature>/{index,service,model}.ts` — controller / plain static-class
|
|
9
|
+
service / `t` schemas. Services never touch `Context`; one Elysia instance is one
|
|
10
|
+
controller.
|
|
11
|
+
- `src/app.ts` is the Eden boundary: no Bun globals, no `process.env`, deps injected
|
|
12
|
+
via `createApp`.
|
|
13
|
+
- Wire values are JSON-shaped (`createdAt` is an ISO string, not `Date`) so Eden's
|
|
14
|
+
types match what the client receives.
|
|
15
|
+
- `src/db/auth-schema.ts` is hand-maintained in the better-auth **1.7** shape
|
|
16
|
+
(`account.issuer`). `@better-auth/cli generate` lags the library — don't use it.
|
|
17
|
+
- Elysia's `status(204)` works under Bun but 500s under Node (vitest); return
|
|
18
|
+
`new Response(null, { status: 204 })` so tests pass.
|
|
19
|
+
- Schema change → `bun run db:generate`, commit `drizzle/`.
|
|
20
|
+
|
|
21
|
+
<!-- redis:start -->
|
|
22
|
+
|
|
23
|
+
- `REDIS_URL` is optional: when set, `src/redis.ts` (Bun's built-in `RedisClient`)
|
|
24
|
+
becomes Better Auth's `secondaryStorage` for sessions and rate limiting.
|
|
25
|
+
|
|
26
|
+
<!-- redis:end -->
|
|
27
|
+
|
|
28
|
+
- Deploy target is Railway (Railpack, no Dockerfile): service root `apps/api`,
|
|
29
|
+
`start` runs migrations then serves. `BETTER_AUTH_URL` must be the public https
|
|
30
|
+
origin — Better Auth derives the secure cookie flag from it.
|
|
31
|
+
- `@repo/api` is a **dev**Dependency of the web app: the import is type-only, and as
|
|
32
|
+
a runtime dep it drags elysia/drizzle into the web production graph.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
CREATE TABLE "note" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"title" text NOT NULL,
|
|
4
|
+
"user_id" text NOT NULL,
|
|
5
|
+
"created_at" timestamp DEFAULT now() NOT NULL
|
|
6
|
+
);
|
|
7
|
+
--> statement-breakpoint
|
|
8
|
+
CREATE TABLE "account" (
|
|
9
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
10
|
+
"issuer" text NOT NULL,
|
|
11
|
+
"account_id" text NOT NULL,
|
|
12
|
+
"provider_id" text NOT NULL,
|
|
13
|
+
"user_id" text NOT NULL,
|
|
14
|
+
"access_token" text,
|
|
15
|
+
"refresh_token" text,
|
|
16
|
+
"id_token" text,
|
|
17
|
+
"access_token_expires_at" timestamp,
|
|
18
|
+
"refresh_token_expires_at" timestamp,
|
|
19
|
+
"scope" text,
|
|
20
|
+
"password" text,
|
|
21
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
22
|
+
"updated_at" timestamp NOT NULL
|
|
23
|
+
);
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
CREATE TABLE "session" (
|
|
26
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
27
|
+
"expires_at" timestamp NOT NULL,
|
|
28
|
+
"token" text NOT NULL,
|
|
29
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
30
|
+
"updated_at" timestamp NOT NULL,
|
|
31
|
+
"ip_address" text,
|
|
32
|
+
"user_agent" text,
|
|
33
|
+
"user_id" text NOT NULL,
|
|
34
|
+
CONSTRAINT "session_token_unique" UNIQUE("token")
|
|
35
|
+
);
|
|
36
|
+
--> statement-breakpoint
|
|
37
|
+
CREATE TABLE "user" (
|
|
38
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
39
|
+
"name" text NOT NULL,
|
|
40
|
+
"email" text NOT NULL,
|
|
41
|
+
"email_verified" boolean DEFAULT false NOT NULL,
|
|
42
|
+
"image" text,
|
|
43
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
44
|
+
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
45
|
+
CONSTRAINT "user_email_unique" UNIQUE("email")
|
|
46
|
+
);
|
|
47
|
+
--> statement-breakpoint
|
|
48
|
+
CREATE TABLE "verification" (
|
|
49
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
50
|
+
"identifier" text NOT NULL,
|
|
51
|
+
"value" text NOT NULL,
|
|
52
|
+
"expires_at" timestamp NOT NULL,
|
|
53
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
54
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
55
|
+
);
|
|
56
|
+
--> statement-breakpoint
|
|
57
|
+
ALTER TABLE "note" ADD CONSTRAINT "note_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
58
|
+
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
59
|
+
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
60
|
+
CREATE INDEX "note_userId_idx" ON "note" USING btree ("user_id");--> statement-breakpoint
|
|
61
|
+
CREATE UNIQUE INDEX "account_issuer_accountId_uidx" ON "account" USING btree ("issuer","account_id");--> statement-breakpoint
|
|
62
|
+
CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
|
|
63
|
+
CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
|
|
64
|
+
CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");
|