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.
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectOptions } from '../scaffold.js';
2
+ export declare function scaffoldBackend(root: string, { projectName, displayName, frontendUrl }: ProjectOptions): Promise<void>;