anhedral 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/LICENSE +22 -0
- package/README.md +65 -0
- package/anhedral.svg +4 -0
- package/bin/anhedral.js +2 -0
- package/dist/branding.d.ts +3 -0
- package/dist/branding.js +9 -0
- package/dist/commands.d.ts +12 -0
- package/dist/commands.js +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +157 -0
- package/dist/scaffold.d.ts +25 -0
- package/dist/scaffold.js +629 -0
- package/dist/templates/backend.d.ts +2 -0
- package/dist/templates/backend.js +2337 -0
- package/dist/templates/default-web.d.ts +2 -0
- package/dist/templates/default-web.js +1368 -0
- package/dist/templates/extension.d.ts +2 -0
- package/dist/templates/extension.js +457 -0
- package/dist/templates/frontend.d.ts +2 -0
- package/dist/templates/frontend.js +1605 -0
- package/dist/toolchain.d.ts +14 -0
- package/dist/toolchain.js +34 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.js +99 -0
- package/package.json +60 -0
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { env } from 'node:process';
|
|
4
|
+
import { appendGitignore, exec, execWithInput, liftNestedProject, writeFile } from './util.js';
|
|
5
|
+
import { getDefaultWebDependencyCommands, getDefaultWebInitCommand, getHeadlessInstallCommands, getSkillCommands, } from './commands.js';
|
|
6
|
+
import { resolveToolchain } from './toolchain.js';
|
|
7
|
+
import { scaffoldBackend } from './templates/backend.js';
|
|
8
|
+
import { writeDefaultWebEnvExample, writeDefaultWebFiles } from './templates/default-web.js';
|
|
9
|
+
import { scaffoldFrontend } from './templates/frontend.js';
|
|
10
|
+
export const STACK_IDS = ['next', 'next-fullstack', 'expo-fullstack', 'backend'];
|
|
11
|
+
const PNPM_PACKAGE_MANAGER = 'pnpm@10.15.1';
|
|
12
|
+
const SHARED_ENV_GITIGNORE_LINES = ['.env', '.env.*', '!.env.example'];
|
|
13
|
+
const SHARED_TYPESCRIPT_GITIGNORE_LINES = ['*.tsbuildinfo'];
|
|
14
|
+
const ASCII_LOGO = [
|
|
15
|
+
' .:-:.',
|
|
16
|
+
' .----:.',
|
|
17
|
+
' .-------:.',
|
|
18
|
+
' :---------:..',
|
|
19
|
+
' :--------..--:.',
|
|
20
|
+
' :-------. :----::.',
|
|
21
|
+
' :------: :-------:.',
|
|
22
|
+
' :------: .---------::',
|
|
23
|
+
' :-------. :::::::::.',
|
|
24
|
+
' :-------.',
|
|
25
|
+
' :-----:.',
|
|
26
|
+
' :----:.',
|
|
27
|
+
' :-:.',
|
|
28
|
+
];
|
|
29
|
+
const ASCII_LOGO_LINE_DELAY_MS = 30;
|
|
30
|
+
async function printAsciiLogo() {
|
|
31
|
+
console.log('');
|
|
32
|
+
for (let index = 0; index < ASCII_LOGO.length; index += 1) {
|
|
33
|
+
console.log(ASCII_LOGO[index]);
|
|
34
|
+
if (index < ASCII_LOGO.length - 1) {
|
|
35
|
+
await new Promise((resolve) => {
|
|
36
|
+
setTimeout(resolve, ASCII_LOGO_LINE_DELAY_MS);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log('');
|
|
41
|
+
}
|
|
42
|
+
function normalizeStack(options, generatedPaths) {
|
|
43
|
+
const toolchain = resolveToolchain(options.toolchainChannel);
|
|
44
|
+
const frontend = options.mode === 'next'
|
|
45
|
+
? 'nextjs_shadcn'
|
|
46
|
+
: options.mode === 'next-fullstack'
|
|
47
|
+
? 'nextjs_shadcn'
|
|
48
|
+
: options.mode === 'expo-fullstack'
|
|
49
|
+
? 'expo_react_native_reusables'
|
|
50
|
+
: null;
|
|
51
|
+
const backend = options.mode === 'next' ? null : 'fastify';
|
|
52
|
+
return {
|
|
53
|
+
schema_version: '2.0.0',
|
|
54
|
+
mode: options.mode,
|
|
55
|
+
project_name: options.projectName,
|
|
56
|
+
display_name: options.displayName,
|
|
57
|
+
frontend,
|
|
58
|
+
backend,
|
|
59
|
+
auth: options.auth,
|
|
60
|
+
payments: options.payments,
|
|
61
|
+
storage: 'cloudflare_r2_via_aws_s3_sdk',
|
|
62
|
+
database: 'neon_plus_drizzle',
|
|
63
|
+
skills: getSkillCommands(options.mode),
|
|
64
|
+
outputs: {
|
|
65
|
+
monorepo: options.monorepo,
|
|
66
|
+
package_manager: 'pnpm',
|
|
67
|
+
toolchain_channel: options.toolchainChannel,
|
|
68
|
+
toolchain,
|
|
69
|
+
generated_paths: generatedPaths,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function readJsonFile(filePath) {
|
|
74
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
75
|
+
}
|
|
76
|
+
function writeJsonFile(filePath, payload) {
|
|
77
|
+
writeFile(filePath, JSON.stringify(payload, null, 2) + '\n');
|
|
78
|
+
}
|
|
79
|
+
function runScaffoldCommands(cwd, commands) {
|
|
80
|
+
for (const command of commands) {
|
|
81
|
+
if (env.ANHEDRAL_SKIP_INSTALL === '1' && command.skippable) {
|
|
82
|
+
console.log(` $ ${command.cmd} (skipped via ANHEDRAL_SKIP_INSTALL=1)`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (command.stdinInput == null) {
|
|
86
|
+
exec(command.cmd, cwd);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
execWithInput(command.cmd, cwd, command.stdinInput);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function ensureScaffoldRoot(root) {
|
|
93
|
+
const allowedEntries = new Set(['.git', '.gitignore', '.DS_Store']);
|
|
94
|
+
const unexpected = readdirSync(root).filter((entry) => !allowedEntries.has(entry));
|
|
95
|
+
if (unexpected.length > 0) {
|
|
96
|
+
throw new Error(`Current directory is not empty. Remove existing files before running anhedral init. Found: ${unexpected.join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function writeRootPackageJson(root, projectName, scripts, workspaces, extraFields) {
|
|
100
|
+
const packageJson = {
|
|
101
|
+
name: projectName,
|
|
102
|
+
private: true,
|
|
103
|
+
version: '0.1.0',
|
|
104
|
+
packageManager: PNPM_PACKAGE_MANAGER,
|
|
105
|
+
scripts,
|
|
106
|
+
...extraFields,
|
|
107
|
+
};
|
|
108
|
+
if (workspaces && workspaces.length > 0) {
|
|
109
|
+
packageJson.workspaces = workspaces;
|
|
110
|
+
}
|
|
111
|
+
writeJsonFile(path.join(root, 'package.json'), packageJson);
|
|
112
|
+
}
|
|
113
|
+
function writePnpmWorkspace(root, packages) {
|
|
114
|
+
const serializedPackages = packages.map((entry) => ` - '${entry}'`).join('\n');
|
|
115
|
+
writeFile(path.join(root, 'pnpm-workspace.yaml'), `packages:\n${serializedPackages}\n`);
|
|
116
|
+
}
|
|
117
|
+
function patchPackageJson(root, updates) {
|
|
118
|
+
const filePath = path.join(root, 'package.json');
|
|
119
|
+
const packageJson = readJsonFile(filePath);
|
|
120
|
+
const scripts = (packageJson.scripts ?? {});
|
|
121
|
+
if (updates.scripts) {
|
|
122
|
+
Object.assign(scripts, updates.scripts);
|
|
123
|
+
}
|
|
124
|
+
packageJson.scripts = scripts;
|
|
125
|
+
if (updates.packageManager) {
|
|
126
|
+
packageJson.packageManager = updates.packageManager;
|
|
127
|
+
}
|
|
128
|
+
writeJsonFile(filePath, packageJson);
|
|
129
|
+
}
|
|
130
|
+
function writeDrizzleConfig(root, schemaPath, migrationsPath) {
|
|
131
|
+
writeFile(path.join(root, 'drizzle.config.ts'), `import type { Config } from 'drizzle-kit';
|
|
132
|
+
|
|
133
|
+
export default {
|
|
134
|
+
schema: '${schemaPath}',
|
|
135
|
+
out: '${migrationsPath}',
|
|
136
|
+
dialect: 'postgresql',
|
|
137
|
+
dbCredentials: {
|
|
138
|
+
url: process.env.DATABASE_URL!,
|
|
139
|
+
},
|
|
140
|
+
} satisfies Config;
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
function writeSkillsGuide(root, commands) {
|
|
144
|
+
const commandBlock = commands.map((command) => `${command}\n`).join('\n');
|
|
145
|
+
writeFile(path.join(root, 'install-skills.sh'), `#!/usr/bin/env bash
|
|
146
|
+
# Manual skill installation guide for this project.
|
|
147
|
+
#
|
|
148
|
+
# Run the commands below after setup. The skills CLI will prompt you to:
|
|
149
|
+
# - choose which agents to install to
|
|
150
|
+
# - choose whether the install should be project-scoped or global
|
|
151
|
+
#
|
|
152
|
+
# If you want the skill files tracked with this project, choose the project scope.
|
|
153
|
+
# Run each command one at a time if you want to review each prompt separately.
|
|
154
|
+
|
|
155
|
+
${commandBlock}`);
|
|
156
|
+
}
|
|
157
|
+
function writeCrossplatformEnvExample(root) {
|
|
158
|
+
writeFile(path.join(root, '.env.example'), `# Shared services
|
|
159
|
+
DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require
|
|
160
|
+
|
|
161
|
+
# Clerk
|
|
162
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
163
|
+
CLERK_SECRET_KEY=sk_test_***
|
|
164
|
+
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
165
|
+
|
|
166
|
+
# Cloudflare R2
|
|
167
|
+
R2_ACCOUNT_ID=
|
|
168
|
+
R2_ACCESS_KEY_ID=
|
|
169
|
+
R2_SECRET_ACCESS_KEY=
|
|
170
|
+
R2_BUCKET=
|
|
171
|
+
|
|
172
|
+
# RevenueCat
|
|
173
|
+
RC_SECRET_API_KEY=
|
|
174
|
+
RC_WEBHOOK_SECRET=
|
|
175
|
+
RC_ENTITLEMENT_ID=pro
|
|
176
|
+
RC_OFFERING_ID=default
|
|
177
|
+
EXPO_PUBLIC_RC_ENTITLEMENT_ID=pro
|
|
178
|
+
EXPO_PUBLIC_RC_API_KEY_IOS=
|
|
179
|
+
EXPO_PUBLIC_RC_API_KEY_ANDROID=
|
|
180
|
+
EXPO_PUBLIC_RC_WEB_API_KEY=
|
|
181
|
+
|
|
182
|
+
# Stripe
|
|
183
|
+
STRIPE_SECRET_KEY=sk_test_***
|
|
184
|
+
STRIPE_WEBHOOK_SECRET=whsec_***
|
|
185
|
+
`);
|
|
186
|
+
}
|
|
187
|
+
function writeNextFullstackRootEnvExample(root) {
|
|
188
|
+
writeFile(path.join(root, '.env.example'), `# Frontend
|
|
189
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
190
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
191
|
+
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
|
192
|
+
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
|
193
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
|
|
194
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
|
|
195
|
+
|
|
196
|
+
# Backend
|
|
197
|
+
PORT=8787
|
|
198
|
+
FRONTEND_URL=http://localhost:3000
|
|
199
|
+
DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require
|
|
200
|
+
CLERK_SECRET_KEY=sk_test_***
|
|
201
|
+
CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
202
|
+
|
|
203
|
+
# Cloudflare R2
|
|
204
|
+
R2_ACCOUNT_ID=
|
|
205
|
+
R2_ACCESS_KEY_ID=
|
|
206
|
+
R2_SECRET_ACCESS_KEY=
|
|
207
|
+
R2_BUCKET=
|
|
208
|
+
|
|
209
|
+
# Stripe
|
|
210
|
+
STRIPE_SECRET_KEY=sk_test_***
|
|
211
|
+
STRIPE_WEBHOOK_SECRET=whsec_***
|
|
212
|
+
STRIPE_PRICE_STARTER=price_***
|
|
213
|
+
`);
|
|
214
|
+
}
|
|
215
|
+
function writeHeadlessPackageJson(root, projectName) {
|
|
216
|
+
writeRootPackageJson(root, projectName, {
|
|
217
|
+
dev: 'tsx --env-file=.env --watch src/index.ts',
|
|
218
|
+
build: 'tsc',
|
|
219
|
+
typecheck: 'tsc --noEmit',
|
|
220
|
+
'db:generate': 'drizzle-kit generate',
|
|
221
|
+
'db:migrate': 'pnpm tsx src/lib/db/migrate.ts',
|
|
222
|
+
}, undefined, {
|
|
223
|
+
type: 'module',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function writeHeadlessTsConfig(root) {
|
|
227
|
+
writeFile(path.join(root, 'tsconfig.json'), JSON.stringify({
|
|
228
|
+
compilerOptions: {
|
|
229
|
+
target: 'ESNext',
|
|
230
|
+
module: 'NodeNext',
|
|
231
|
+
moduleResolution: 'NodeNext',
|
|
232
|
+
strict: true,
|
|
233
|
+
esModuleInterop: true,
|
|
234
|
+
skipLibCheck: true,
|
|
235
|
+
outDir: 'dist',
|
|
236
|
+
rootDir: 'src',
|
|
237
|
+
types: ['node'],
|
|
238
|
+
},
|
|
239
|
+
include: ['src/**/*'],
|
|
240
|
+
exclude: ['node_modules', 'dist'],
|
|
241
|
+
}, null, 2) + '\n');
|
|
242
|
+
}
|
|
243
|
+
function writeHeadlessEnvExample(root) {
|
|
244
|
+
writeFile(path.join(root, '.env.example'), `PORT=8787
|
|
245
|
+
CLIENT_ORIGIN=http://localhost:3000
|
|
246
|
+
DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require
|
|
247
|
+
|
|
248
|
+
# Better Auth
|
|
249
|
+
BETTER_AUTH_SECRET=replace-me
|
|
250
|
+
BETTER_AUTH_URL=http://localhost:8787
|
|
251
|
+
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000
|
|
252
|
+
|
|
253
|
+
# Cloudflare R2
|
|
254
|
+
R2_ACCOUNT_ID=
|
|
255
|
+
R2_ACCESS_KEY_ID=
|
|
256
|
+
R2_SECRET_ACCESS_KEY=
|
|
257
|
+
R2_BUCKET=
|
|
258
|
+
`);
|
|
259
|
+
}
|
|
260
|
+
function writeHeadlessFiles(root) {
|
|
261
|
+
writeFile(path.join(root, 'src/lib/db/client.ts'), `import { drizzle } from 'drizzle-orm/neon-http';
|
|
262
|
+
import { neon } from '@neondatabase/serverless';
|
|
263
|
+
import * as schema from './schema.js';
|
|
264
|
+
|
|
265
|
+
if (!process.env.DATABASE_URL) {
|
|
266
|
+
throw new Error('DATABASE_URL is not defined');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const sql = neon(process.env.DATABASE_URL);
|
|
270
|
+
export const db = drizzle(sql, { schema });
|
|
271
|
+
`);
|
|
272
|
+
writeFile(path.join(root, 'src/lib/db/schema.ts'), `import { pgTable, text, timestamp, index } from 'drizzle-orm/pg-core';
|
|
273
|
+
|
|
274
|
+
export const users = pgTable('users', {
|
|
275
|
+
id: text('id').primaryKey(),
|
|
276
|
+
email: text('email').notNull().unique(),
|
|
277
|
+
name: text('name'),
|
|
278
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
279
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
280
|
+
}, (table) => [
|
|
281
|
+
index('users_email_idx').on(table.email),
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
export const uploads = pgTable('uploads', {
|
|
285
|
+
id: text('id').primaryKey(),
|
|
286
|
+
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
287
|
+
objectKey: text('object_key').notNull().unique(),
|
|
288
|
+
bucket: text('bucket').notNull(),
|
|
289
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
290
|
+
});
|
|
291
|
+
`);
|
|
292
|
+
writeFile(path.join(root, 'src/lib/db/queries/users.ts'), `import { eq } from 'drizzle-orm';
|
|
293
|
+
import { db } from '../client.js';
|
|
294
|
+
import { users } from '../schema.js';
|
|
295
|
+
|
|
296
|
+
export async function findUserByEmail(email: string) {
|
|
297
|
+
return db.query.users.findFirst({
|
|
298
|
+
where: eq(users.email, email),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
`);
|
|
302
|
+
writeFile(path.join(root, 'src/lib/db/queries/index.ts'), `export * from './users.js';
|
|
303
|
+
`);
|
|
304
|
+
writeFile(path.join(root, 'src/lib/db/migrate.ts'), `import { drizzle } from 'drizzle-orm/neon-http';
|
|
305
|
+
import { migrate } from 'drizzle-orm/neon-http/migrator';
|
|
306
|
+
import { neon } from '@neondatabase/serverless';
|
|
307
|
+
import 'dotenv/config';
|
|
308
|
+
|
|
309
|
+
if (!process.env.DATABASE_URL) {
|
|
310
|
+
throw new Error('DATABASE_URL is not defined');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const sql = neon(process.env.DATABASE_URL);
|
|
314
|
+
const db = drizzle(sql);
|
|
315
|
+
|
|
316
|
+
await migrate(db, { migrationsFolder: './src/lib/db/migrations' });
|
|
317
|
+
`);
|
|
318
|
+
writeFile(path.join(root, 'src/lib/db/migrations/.gitkeep'), '');
|
|
319
|
+
writeFile(path.join(root, 'src/lib/storage/r2.ts'), `import { S3Client } from '@aws-sdk/client-s3';
|
|
320
|
+
|
|
321
|
+
export const r2 = new S3Client({
|
|
322
|
+
region: 'auto',
|
|
323
|
+
endpoint: \`https://\${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com\`,
|
|
324
|
+
credentials: {
|
|
325
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? '',
|
|
326
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? '',
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
`);
|
|
330
|
+
writeFile(path.join(root, 'src/lib/auth/auth.ts'), `import { betterAuth } from 'better-auth';
|
|
331
|
+
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
332
|
+
import { db } from '../db/client.js';
|
|
333
|
+
import * as schema from '../db/schema.js';
|
|
334
|
+
|
|
335
|
+
export const auth = betterAuth({
|
|
336
|
+
database: drizzleAdapter(db, {
|
|
337
|
+
provider: 'pg',
|
|
338
|
+
usePlural: true,
|
|
339
|
+
schema,
|
|
340
|
+
}),
|
|
341
|
+
emailAndPassword: {
|
|
342
|
+
enabled: true,
|
|
343
|
+
},
|
|
344
|
+
baseURL: process.env.BETTER_AUTH_URL,
|
|
345
|
+
trustedOrigins: (process.env.BETTER_AUTH_TRUSTED_ORIGINS ?? '')
|
|
346
|
+
.split(',')
|
|
347
|
+
.map((value) => value.trim())
|
|
348
|
+
.filter(Boolean),
|
|
349
|
+
});
|
|
350
|
+
`);
|
|
351
|
+
writeFile(path.join(root, 'src/app.ts'), `import Fastify from 'fastify';
|
|
352
|
+
import cors from '@fastify/cors';
|
|
353
|
+
import { fromNodeHeaders } from 'better-auth/node';
|
|
354
|
+
import { auth } from './lib/auth/auth.js';
|
|
355
|
+
|
|
356
|
+
export function buildApp() {
|
|
357
|
+
const app = Fastify({ logger: true });
|
|
358
|
+
|
|
359
|
+
app.register(cors, {
|
|
360
|
+
origin: process.env.CLIENT_ORIGIN ?? true,
|
|
361
|
+
credentials: true,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
app.get('/health', async () => ({ ok: true }));
|
|
365
|
+
|
|
366
|
+
app.route({
|
|
367
|
+
method: ['GET', 'POST'],
|
|
368
|
+
url: '/api/auth/*',
|
|
369
|
+
async handler(request, reply) {
|
|
370
|
+
const url = new URL(request.url, \`http://\${request.headers.host}\`);
|
|
371
|
+
const headers = new Headers();
|
|
372
|
+
|
|
373
|
+
Object.entries(request.headers).forEach(([key, value]) => {
|
|
374
|
+
if (Array.isArray(value)) {
|
|
375
|
+
for (const entry of value) headers.append(key, entry);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (value) headers.append(key, value.toString());
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const authRequest = new Request(url.toString(), {
|
|
383
|
+
method: request.method,
|
|
384
|
+
headers,
|
|
385
|
+
body: request.body ? JSON.stringify(request.body) : undefined,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const response = await auth.handler(authRequest);
|
|
389
|
+
reply.status(response.status);
|
|
390
|
+
response.headers.forEach((value, key) => reply.header(key, value));
|
|
391
|
+
reply.send(response.body ? await response.text() : null);
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
app.get('/api/me', async (request, reply) => {
|
|
396
|
+
const session = await auth.api.getSession({
|
|
397
|
+
headers: fromNodeHeaders(request.headers),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (!session) {
|
|
401
|
+
return reply.status(401).send({ error: 'Unauthorized' });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return reply.send(session);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return app;
|
|
408
|
+
}
|
|
409
|
+
`);
|
|
410
|
+
writeFile(path.join(root, 'src/index.ts'), `import 'dotenv/config';
|
|
411
|
+
import { buildApp } from './app.js';
|
|
412
|
+
|
|
413
|
+
const app = buildApp();
|
|
414
|
+
const port = Number(process.env.PORT ?? 8787);
|
|
415
|
+
|
|
416
|
+
app.listen({ port, host: '0.0.0.0' })
|
|
417
|
+
.then(() => {
|
|
418
|
+
app.log.info({ port }, 'server_started');
|
|
419
|
+
})
|
|
420
|
+
.catch((error) => {
|
|
421
|
+
app.log.error(error);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
});
|
|
424
|
+
`);
|
|
425
|
+
}
|
|
426
|
+
function writeRootDocs(root, options, stack) {
|
|
427
|
+
const toolchainLine = stack.outputs.toolchain.verifiedAt
|
|
428
|
+
? `${options.toolchainChannel} (verified ${stack.outputs.toolchain.verifiedAt})`
|
|
429
|
+
: `${options.toolchainChannel} (floating latest)`;
|
|
430
|
+
const generatedPaths = stack.outputs.generated_paths.map((entry) => `- \`${entry}\``).join('\n') || '- `.`';
|
|
431
|
+
const envCopyStep = options.mode === 'next'
|
|
432
|
+
? 'Copy `.env.example` to `.env.local` and fill in your provider values.'
|
|
433
|
+
: options.mode === 'next-fullstack'
|
|
434
|
+
? 'Copy the root `.env.example` values into the `.env.local` and `.env` files your frontend and backend will read before starting the apps.'
|
|
435
|
+
: options.mode === 'expo-fullstack'
|
|
436
|
+
? 'Copy the generated `.env.example` files into the runtime `.env` files each package expects before starting the apps.'
|
|
437
|
+
: 'Copy `.env.example` to `.env` and fill in your provider values.';
|
|
438
|
+
const authStep = options.mode === 'backend'
|
|
439
|
+
? 'Review `src/lib/auth/auth.ts` and align the Better Auth schema with your production auth tables before shipping.'
|
|
440
|
+
: options.mode === 'expo-fullstack'
|
|
441
|
+
? 'Add the real provider configuration for Clerk, RevenueCat, and Stripe where the scaffolded helper files indicate.'
|
|
442
|
+
: options.mode === 'next-fullstack'
|
|
443
|
+
? 'Add the real provider configuration for Clerk and Stripe across the frontend and backend where the scaffolded helper files indicate.'
|
|
444
|
+
: 'Add the real provider configuration for Clerk and Stripe where the scaffolded helper files indicate.';
|
|
445
|
+
const databaseCommands = options.mode === 'next-fullstack' || options.mode === 'expo-fullstack'
|
|
446
|
+
? '```bash\ncd backend\npnpm db:generate\npnpm db:migrate\n```'
|
|
447
|
+
: '```bash\npnpm db:generate\npnpm db:migrate\n```';
|
|
448
|
+
writeFile(path.join(root, 'README.md'), `# ${options.displayName}
|
|
449
|
+
|
|
450
|
+
Generated by anhedral.
|
|
451
|
+
|
|
452
|
+
## Stack
|
|
453
|
+
|
|
454
|
+
- Mode: ${options.mode}
|
|
455
|
+
- Auth: ${options.auth}
|
|
456
|
+
- Payments: ${options.payments}
|
|
457
|
+
- Database: ${options.db} + ${options.orm}
|
|
458
|
+
- Storage: ${options.storage}
|
|
459
|
+
- API: ${options.api ?? 'framework-native'}
|
|
460
|
+
- Toolchain: ${toolchainLine}
|
|
461
|
+
|
|
462
|
+
## Generated paths
|
|
463
|
+
|
|
464
|
+
${generatedPaths}
|
|
465
|
+
|
|
466
|
+
## Setup
|
|
467
|
+
|
|
468
|
+
1. ${envCopyStep}
|
|
469
|
+
2. Review \`install-skills.sh\` and run the listed skill commands manually so you can choose scope and agent targets.
|
|
470
|
+
3. ${authStep}
|
|
471
|
+
4. Run database generation and migrations:
|
|
472
|
+
|
|
473
|
+
${databaseCommands}
|
|
474
|
+
`);
|
|
475
|
+
}
|
|
476
|
+
function writeStackFile(root, stack) {
|
|
477
|
+
writeFile(path.join(root, 'stack.json'), JSON.stringify(stack, null, 2) + '\n');
|
|
478
|
+
}
|
|
479
|
+
function scaffoldDefaultWeb(root, options) {
|
|
480
|
+
const toolchain = resolveToolchain(options.toolchainChannel);
|
|
481
|
+
runScaffoldCommands(root, [getDefaultWebInitCommand(toolchain, options.projectName)]);
|
|
482
|
+
liftNestedProject(root, options.projectName);
|
|
483
|
+
runScaffoldCommands(root, getDefaultWebDependencyCommands());
|
|
484
|
+
appendGitignore(root, [
|
|
485
|
+
...SHARED_ENV_GITIGNORE_LINES,
|
|
486
|
+
'.next',
|
|
487
|
+
'node_modules',
|
|
488
|
+
'lib/db/migrations',
|
|
489
|
+
...SHARED_TYPESCRIPT_GITIGNORE_LINES,
|
|
490
|
+
]);
|
|
491
|
+
patchPackageJson(root, {
|
|
492
|
+
packageManager: PNPM_PACKAGE_MANAGER,
|
|
493
|
+
scripts: {
|
|
494
|
+
typecheck: 'tsc --noEmit',
|
|
495
|
+
'db:generate': 'drizzle-kit generate',
|
|
496
|
+
'db:migrate': 'pnpm tsx lib/db/migrate.ts',
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
writeDrizzleConfig(root, './lib/db/schema.ts', './lib/db/migrations');
|
|
500
|
+
writeDefaultWebEnvExample(root);
|
|
501
|
+
writeDefaultWebFiles(root, options.displayName);
|
|
502
|
+
return ['.'];
|
|
503
|
+
}
|
|
504
|
+
function patchGeneratedPackageName(root, name) {
|
|
505
|
+
const filePath = path.join(root, 'package.json');
|
|
506
|
+
const packageJson = readJsonFile(filePath);
|
|
507
|
+
packageJson.name = name;
|
|
508
|
+
writeJsonFile(filePath, packageJson);
|
|
509
|
+
}
|
|
510
|
+
function scaffoldNextFrontend(root, options, directoryName) {
|
|
511
|
+
const toolchain = resolveToolchain(options.toolchainChannel);
|
|
512
|
+
runScaffoldCommands(root, [getDefaultWebInitCommand(toolchain, directoryName)]);
|
|
513
|
+
runScaffoldCommands(path.join(root, directoryName), getDefaultWebDependencyCommands());
|
|
514
|
+
appendGitignore(path.join(root, directoryName), [
|
|
515
|
+
...SHARED_ENV_GITIGNORE_LINES,
|
|
516
|
+
'.next',
|
|
517
|
+
'node_modules',
|
|
518
|
+
'lib/db/migrations',
|
|
519
|
+
...SHARED_TYPESCRIPT_GITIGNORE_LINES,
|
|
520
|
+
]);
|
|
521
|
+
patchPackageJson(path.join(root, directoryName), {
|
|
522
|
+
packageManager: PNPM_PACKAGE_MANAGER,
|
|
523
|
+
scripts: {
|
|
524
|
+
typecheck: 'tsc --noEmit',
|
|
525
|
+
'db:generate': 'drizzle-kit generate',
|
|
526
|
+
'db:migrate': 'pnpm tsx lib/db/migrate.ts',
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
patchGeneratedPackageName(path.join(root, directoryName), `${options.projectName}-${directoryName}`);
|
|
530
|
+
writeDrizzleConfig(path.join(root, directoryName), './lib/db/schema.ts', './lib/db/migrations');
|
|
531
|
+
writeDefaultWebEnvExample(path.join(root, directoryName));
|
|
532
|
+
writeDefaultWebFiles(path.join(root, directoryName), options.displayName);
|
|
533
|
+
}
|
|
534
|
+
async function scaffoldNextFullstack(root, options) {
|
|
535
|
+
scaffoldNextFrontend(root, options, 'frontend');
|
|
536
|
+
writeRootPackageJson(root, options.projectName, {
|
|
537
|
+
dev: 'pnpm -r --parallel --filter ./frontend --filter ./backend run dev',
|
|
538
|
+
}, ['frontend', 'backend']);
|
|
539
|
+
writePnpmWorkspace(root, ['frontend', 'backend']);
|
|
540
|
+
appendGitignore(root, [
|
|
541
|
+
'node_modules',
|
|
542
|
+
'frontend/node_modules',
|
|
543
|
+
'backend/node_modules',
|
|
544
|
+
...SHARED_ENV_GITIGNORE_LINES,
|
|
545
|
+
]);
|
|
546
|
+
writeNextFullstackRootEnvExample(root);
|
|
547
|
+
await scaffoldBackend(root, {
|
|
548
|
+
projectName: options.projectName,
|
|
549
|
+
displayName: options.displayName,
|
|
550
|
+
githubOrg: null,
|
|
551
|
+
frontendUrl: 'http://localhost:3000',
|
|
552
|
+
});
|
|
553
|
+
return ['frontend', 'backend'];
|
|
554
|
+
}
|
|
555
|
+
async function scaffoldCrossplatform(root, options) {
|
|
556
|
+
writeRootPackageJson(root, options.projectName, {
|
|
557
|
+
dev: 'pnpm -r --parallel --filter ./frontend --filter ./backend run dev',
|
|
558
|
+
}, ['frontend', 'backend']);
|
|
559
|
+
writePnpmWorkspace(root, ['frontend', 'backend']);
|
|
560
|
+
appendGitignore(root, [
|
|
561
|
+
'node_modules',
|
|
562
|
+
'frontend/node_modules',
|
|
563
|
+
'backend/node_modules',
|
|
564
|
+
...SHARED_ENV_GITIGNORE_LINES,
|
|
565
|
+
]);
|
|
566
|
+
writeCrossplatformEnvExample(root);
|
|
567
|
+
const projectOptions = {
|
|
568
|
+
projectName: options.projectName,
|
|
569
|
+
displayName: options.displayName,
|
|
570
|
+
githubOrg: null,
|
|
571
|
+
frontendUrl: 'http://localhost:8081',
|
|
572
|
+
};
|
|
573
|
+
await scaffoldFrontend(root, projectOptions);
|
|
574
|
+
await scaffoldBackend(root, projectOptions);
|
|
575
|
+
return ['frontend', 'backend'];
|
|
576
|
+
}
|
|
577
|
+
function scaffoldHeadless(root, options) {
|
|
578
|
+
writeHeadlessPackageJson(root, options.projectName);
|
|
579
|
+
appendGitignore(root, [
|
|
580
|
+
'dist',
|
|
581
|
+
'node_modules',
|
|
582
|
+
...SHARED_ENV_GITIGNORE_LINES,
|
|
583
|
+
'*.log',
|
|
584
|
+
...SHARED_TYPESCRIPT_GITIGNORE_LINES,
|
|
585
|
+
]);
|
|
586
|
+
writeHeadlessTsConfig(root);
|
|
587
|
+
writeDrizzleConfig(root, './src/lib/db/schema.ts', './src/lib/db/migrations');
|
|
588
|
+
writeHeadlessEnvExample(root);
|
|
589
|
+
writeHeadlessFiles(root);
|
|
590
|
+
runScaffoldCommands(root, getHeadlessInstallCommands());
|
|
591
|
+
return ['.'];
|
|
592
|
+
}
|
|
593
|
+
export async function scaffoldProject(options) {
|
|
594
|
+
const root = path.resolve(process.cwd());
|
|
595
|
+
ensureScaffoldRoot(root);
|
|
596
|
+
const previousToolchain = env.ANHEDRAL_TOOLCHAIN;
|
|
597
|
+
env.ANHEDRAL_TOOLCHAIN = options.toolchainChannel;
|
|
598
|
+
try {
|
|
599
|
+
await printAsciiLogo();
|
|
600
|
+
console.log(`\nš Initializing ${options.displayName} in ${root}\n`);
|
|
601
|
+
const generatedPaths = options.mode === 'next'
|
|
602
|
+
? scaffoldDefaultWeb(root, options)
|
|
603
|
+
: options.mode === 'next-fullstack'
|
|
604
|
+
? await scaffoldNextFullstack(root, options)
|
|
605
|
+
: options.mode === 'expo-fullstack'
|
|
606
|
+
? await scaffoldCrossplatform(root, options)
|
|
607
|
+
: scaffoldHeadless(root, options);
|
|
608
|
+
const skillCommands = getSkillCommands(options.mode);
|
|
609
|
+
writeSkillsGuide(root, skillCommands);
|
|
610
|
+
const stack = normalizeStack(options, generatedPaths);
|
|
611
|
+
writeRootDocs(root, options, stack);
|
|
612
|
+
writeStackFile(root, stack);
|
|
613
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
614
|
+
console.log(`\nš ${options.displayName} is ready.\n`);
|
|
615
|
+
console.log('Generated:');
|
|
616
|
+
for (const generatedPath of generatedPaths) {
|
|
617
|
+
console.log(` - ${generatedPath}`);
|
|
618
|
+
}
|
|
619
|
+
console.log('');
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
if (previousToolchain == null) {
|
|
623
|
+
delete env.ANHEDRAL_TOOLCHAIN;
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
env.ANHEDRAL_TOOLCHAIN = previousToolchain;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|