anhedral 0.1.3 → 0.1.5
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 +136 -20
- package/dist/cli.d.ts +3 -5
- package/dist/cli.js +8 -27
- package/dist/commands.d.ts +3 -7
- package/dist/commands.js +12 -38
- package/dist/dependencies.d.ts +35 -0
- package/dist/dependencies.js +173 -0
- package/dist/scaffold.d.ts +6 -7
- package/dist/scaffold.js +409 -296
- package/dist/templates/backend.d.ts +1 -1
- package/dist/templates/backend.js +36 -45
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +81 -62
- package/dist/templates/frontend.d.ts +1 -1
- package/dist/templates/frontend.js +135 -179
- package/dist/toolchain.js +5 -7
- package/dist/util.d.ts +1 -0
- package/dist/util.js +25 -1
- package/package.json +5 -4
- package/dist/templates/default-web.d.ts +0 -2
- package/dist/templates/default-web.js +0 -1950
package/dist/scaffold.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { env } from 'node:process';
|
|
4
4
|
import { anhedralPrint } from './print.js';
|
|
5
|
-
import { appendGitignore,
|
|
6
|
-
import {
|
|
5
|
+
import { appendGitignore, writeFile } from './util.js';
|
|
6
|
+
import { getSkillCommands } from './commands.js';
|
|
7
|
+
import { API_CLIENT_DEPENDENCIES, PACKAGE_MANAGER, ROOT_DEPENDENCIES, SHARED_DB_DEPENDENCIES, SHARED_PACKAGE_DEPENDENCIES, dependencyManifest, } from './dependencies.js';
|
|
7
8
|
import { resolveToolchain } from './toolchain.js';
|
|
8
9
|
import { scaffoldBackend } from './templates/backend.js';
|
|
9
|
-
import { writeDefaultWebEnvExample, writeDefaultWebFiles } from './templates/default-web.js';
|
|
10
10
|
import { scaffoldExtension } from './templates/extension.js';
|
|
11
11
|
import { scaffoldFrontend } from './templates/frontend.js';
|
|
12
|
-
const PNPM_PACKAGE_MANAGER = 'pnpm@10.15.1';
|
|
13
12
|
const SHARED_ENV_GITIGNORE_LINES = ['.env', '.env.*', '!.env.example'];
|
|
14
13
|
const SHARED_TYPESCRIPT_GITIGNORE_LINES = ['*.tsbuildinfo'];
|
|
15
14
|
const ASCII_LOGO = [
|
|
@@ -43,61 +42,39 @@ async function printAsciiLogo() {
|
|
|
43
42
|
function getUsedToolchain(options, toolchain) {
|
|
44
43
|
return {
|
|
45
44
|
verifiedAt: toolchain.verifiedAt,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
shadcn: toolchain.shadcn,
|
|
46
|
+
reactNativeReusables: toolchain.reactNativeReusables,
|
|
47
|
+
wxt: toolchain.wxt,
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
function normalizeStack(options, generatedPaths) {
|
|
52
51
|
const toolchain = resolveToolchain(options.toolchainChannel);
|
|
53
|
-
const frontend = options.frontend === 'next'
|
|
54
|
-
? 'nextjs_shadcn'
|
|
55
|
-
: 'expo_react_native_reusables';
|
|
56
52
|
return {
|
|
57
53
|
schema_version: '2.0.0',
|
|
58
54
|
mode: 'fullstack',
|
|
59
55
|
project_name: options.projectName,
|
|
60
56
|
display_name: options.displayName,
|
|
61
|
-
frontend,
|
|
62
|
-
extension:
|
|
57
|
+
frontend: 'expo_react_native_reusables',
|
|
58
|
+
extension: 'wxt_chrome_extension',
|
|
63
59
|
backend: 'fastify',
|
|
64
60
|
auth: options.auth,
|
|
65
61
|
payments: options.payments,
|
|
66
62
|
storage: 'cloudflare_r2_via_aws_s3_sdk',
|
|
67
63
|
database: 'neon_plus_drizzle',
|
|
68
|
-
skills: getSkillCommands(
|
|
64
|
+
skills: getSkillCommands(),
|
|
69
65
|
outputs: {
|
|
70
|
-
monorepo:
|
|
66
|
+
monorepo: true,
|
|
71
67
|
package_manager: 'pnpm',
|
|
68
|
+
dependency_manifest: dependencyManifest(),
|
|
72
69
|
toolchain_channel: options.toolchainChannel,
|
|
73
70
|
toolchain: getUsedToolchain(options, toolchain),
|
|
74
71
|
generated_paths: generatedPaths,
|
|
75
72
|
},
|
|
76
73
|
};
|
|
77
74
|
}
|
|
78
|
-
function readJsonFile(filePath) {
|
|
79
|
-
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
80
|
-
}
|
|
81
75
|
function writeJsonFile(filePath, payload) {
|
|
82
76
|
writeFile(filePath, JSON.stringify(payload, null, 2) + '\n');
|
|
83
77
|
}
|
|
84
|
-
function runScaffoldCommands(cwd, commands) {
|
|
85
|
-
let skippedCount = 0;
|
|
86
|
-
for (const command of commands) {
|
|
87
|
-
if (env.ANHEDRAL_SKIP_INSTALL === '1' && command.skippable) {
|
|
88
|
-
skippedCount += 1;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (command.stdinInput == null) {
|
|
92
|
-
exec(command.cmd, cwd);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
execWithInput(command.cmd, cwd, command.stdinInput);
|
|
96
|
-
}
|
|
97
|
-
if (skippedCount > 0) {
|
|
98
|
-
anhedralPrint.info(`Skipped ${skippedCount} install step${skippedCount === 1 ? '' : 's'} (ANHEDRAL_SKIP_INSTALL=1)`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
78
|
function ensureScaffoldRoot(root) {
|
|
102
79
|
const allowedEntries = new Set(['.git', '.gitignore', '.DS_Store']);
|
|
103
80
|
const unexpected = readdirSync(root).filter((entry) => !allowedEntries.has(entry));
|
|
@@ -110,7 +87,7 @@ function writeRootPackageJson(root, projectName, scripts, workspaces, extraField
|
|
|
110
87
|
name: projectName,
|
|
111
88
|
private: true,
|
|
112
89
|
version: '0.1.0',
|
|
113
|
-
packageManager:
|
|
90
|
+
packageManager: PACKAGE_MANAGER,
|
|
114
91
|
scripts,
|
|
115
92
|
...extraFields,
|
|
116
93
|
};
|
|
@@ -123,26 +100,6 @@ function writePnpmWorkspace(root, packages) {
|
|
|
123
100
|
const serializedPackages = packages.map((entry) => ` - '${entry}'`).join('\n');
|
|
124
101
|
writeFile(path.join(root, 'pnpm-workspace.yaml'), `packages:\n${serializedPackages}\n`);
|
|
125
102
|
}
|
|
126
|
-
function patchPackageJson(root, updates) {
|
|
127
|
-
const filePath = path.join(root, 'package.json');
|
|
128
|
-
const packageJson = readJsonFile(filePath);
|
|
129
|
-
const scripts = (packageJson.scripts ?? {});
|
|
130
|
-
if (updates.scripts) {
|
|
131
|
-
Object.assign(scripts, updates.scripts);
|
|
132
|
-
}
|
|
133
|
-
packageJson.scripts = scripts;
|
|
134
|
-
if (updates.packageManager) {
|
|
135
|
-
packageJson.packageManager = updates.packageManager;
|
|
136
|
-
}
|
|
137
|
-
writeJsonFile(filePath, packageJson);
|
|
138
|
-
}
|
|
139
|
-
function replaceInFile(filePath, replacements) {
|
|
140
|
-
let contents = readFileSync(filePath, 'utf-8');
|
|
141
|
-
for (const [search, replacement] of replacements) {
|
|
142
|
-
contents = contents.split(search).join(replacement);
|
|
143
|
-
}
|
|
144
|
-
writeFile(filePath, contents);
|
|
145
|
-
}
|
|
146
103
|
function writeSkillsGuide(root, commands) {
|
|
147
104
|
const commandBlock = commands.map((command) => `${command}\n`).join('\n');
|
|
148
105
|
writeFile(path.join(root, 'install-skills.sh'), `#!/usr/bin/env bash
|
|
@@ -162,23 +119,6 @@ function writeRootDocs(root, options, stack) {
|
|
|
162
119
|
? `${options.toolchainChannel} (verified ${stack.outputs.toolchain.verifiedAt})`
|
|
163
120
|
: `${options.toolchainChannel} (floating latest)`;
|
|
164
121
|
const generatedPaths = stack.outputs.generated_paths.map((entry) => `- \`${entry}\``).join('\n') || '- `.`';
|
|
165
|
-
const frontendLabel = options.frontend === 'next' ? 'Next.js' : 'Expo';
|
|
166
|
-
const extensionLine = options.extension ? '- Extension: WXT Chrome extension\n' : '';
|
|
167
|
-
const extensionSetup = options.extension
|
|
168
|
-
? '\n5. Build or run the extension separately. It is not deployed by Vercel:\n\n```bash\npnpm dev:extension\npnpm extension:zip\n```\n'
|
|
169
|
-
: '';
|
|
170
|
-
const frontendDevCommand = options.frontend === 'next' ? 'pnpm dev:web' : 'pnpm dev:mobile';
|
|
171
|
-
const frontendEnvCommand = options.frontend === 'next'
|
|
172
|
-
? 'cp apps/web/.env.example apps/web/.env.local'
|
|
173
|
-
: '# apps/mobile/.env is generated for local Expo development';
|
|
174
|
-
const extensionEnvCommand = options.extension ? '\ncp apps/extension/.env.example apps/extension/.env' : '';
|
|
175
|
-
const extensionDevCommand = options.extension ? '\npnpm dev:extension' : '';
|
|
176
|
-
const paymentSetup = options.payments === 'stripe'
|
|
177
|
-
? `- Stripe: create a Checkout price, then set \`STRIPE_SECRET_KEY\`, \`STRIPE_WEBHOOK_SECRET\`, and \`STRIPE_PRICE_STARTER\`.`
|
|
178
|
-
: `- RevenueCat: create an entitlement named \`pro\`, configure app keys, then set \`RC_SECRET_API_KEY\`, \`RC_WEBHOOK_SECRET\`, and the \`EXPO_PUBLIC_RC_*\` values.`;
|
|
179
|
-
const deploySetup = options.frontend === 'next'
|
|
180
|
-
? '- Vercel: deploy this repo from the root. The root `vercel.json` builds `apps/web` and `apps/api`; Fastify is routed under `/backend/*`.'
|
|
181
|
-
: '- Vercel: deploy this repo from the root when you want the Fastify API hosted. Expo is built and shipped outside Vercel.';
|
|
182
122
|
writeFile(path.join(root, 'README.md'), `# ${options.displayName}
|
|
183
123
|
|
|
184
124
|
Generated by anhedral.
|
|
@@ -186,15 +126,17 @@ Generated by anhedral.
|
|
|
186
126
|
## Stack
|
|
187
127
|
|
|
188
128
|
- Mode: fullstack
|
|
189
|
-
- Frontend:
|
|
129
|
+
- Frontend: Expo + React Native Reusables
|
|
190
130
|
- Backend: Fastify
|
|
191
|
-
|
|
131
|
+
- Extension: WXT Chrome extension
|
|
132
|
+
- Shared packages: \`packages/*\`
|
|
192
133
|
- Auth: ${options.auth}
|
|
193
134
|
- Payments: ${options.payments}
|
|
194
135
|
- Database: ${options.db} + ${options.orm}
|
|
195
136
|
- Storage: ${options.storage}
|
|
196
137
|
- API: ${options.api ?? 'framework-native'}
|
|
197
138
|
- Toolchain: ${toolchainLine}
|
|
139
|
+
- Dependency manifest: recorded in \`stack.json\`
|
|
198
140
|
|
|
199
141
|
## Generated paths
|
|
200
142
|
|
|
@@ -205,35 +147,211 @@ ${generatedPaths}
|
|
|
205
147
|
\`\`\`bash
|
|
206
148
|
pnpm install
|
|
207
149
|
cp .env.example .env
|
|
208
|
-
|
|
150
|
+
# Frontend/.env is generated for local Expo development
|
|
151
|
+
cp Extension/.env.example Extension/.env
|
|
209
152
|
pnpm db:generate
|
|
210
153
|
pnpm db:migrate
|
|
211
|
-
pnpm
|
|
212
|
-
|
|
154
|
+
pnpm verify
|
|
155
|
+
pnpm dev:backend
|
|
156
|
+
pnpm dev:frontend
|
|
157
|
+
pnpm dev:extension
|
|
213
158
|
\`\`\`
|
|
214
159
|
|
|
215
|
-
For a provider-free smoke test, keep \`ANHEDRAL_DEMO=true\` in \`
|
|
160
|
+
For a provider-free smoke test, keep \`ANHEDRAL_DEMO=true\` in \`Backend/.env\`. Demo mode returns a signed-in sample user and active subscription responses without Clerk, RevenueCat, or Stripe credentials. It is for local development only.
|
|
216
161
|
|
|
217
162
|
## Provider Setup
|
|
218
163
|
|
|
219
164
|
- Neon: create a Postgres database and set \`DATABASE_URL\` in the root and API env files.
|
|
220
|
-
- Clerk: create an application, configure allowed origins, and set \`CLERK_PUBLISHABLE_KEY\`, \`CLERK_SECRET_KEY\`, plus the
|
|
221
|
-
|
|
165
|
+
- Clerk: create an application, configure allowed origins, and set \`CLERK_PUBLISHABLE_KEY\`, \`CLERK_SECRET_KEY\`, plus the Expo and extension publishable keys.
|
|
166
|
+
- RevenueCat + Stripe: create an entitlement named \`pro\`, configure Stripe as a RevenueCat web billing source, configure app keys, then set \`RC_SECRET_API_KEY\`, \`RC_WEBHOOK_SECRET\`, and the \`EXPO_PUBLIC_RC_*\` values.
|
|
222
167
|
- Cloudflare R2: create a bucket and API token, then set \`R2_ACCOUNT_ID\`, \`R2_ACCESS_KEY_ID\`, \`R2_SECRET_ACCESS_KEY\`, and \`R2_BUCKET\`.
|
|
223
|
-
|
|
168
|
+
- Vercel: import this same Git repository twice. Use \`Frontend\` as the root directory for the Expo web project, and \`Backend\` as the root directory for the Fastify API project. Enable access to source files outside each root directory so workspace packages under \`packages/*\` are available during builds. The Backend project uses Vercel Fastify entrypoint detection from \`Backend/src/index.ts\`.
|
|
169
|
+
- EAS: use \`Frontend\` as the Expo project root for iOS and Android builds. Vercel deploys the web export from the same source.
|
|
224
170
|
|
|
225
171
|
## Setup Notes
|
|
226
172
|
|
|
227
173
|
1. Copy \`.env.example\` into the runtime env files your apps need and fill in provider values.
|
|
228
174
|
2. Review \`install-skills.sh\` and run the listed skill commands manually so you can choose scope and agent targets.
|
|
229
|
-
3. Add the real provider configuration for Clerk,
|
|
230
|
-
4.
|
|
175
|
+
3. Add the real provider configuration for Clerk, RevenueCat, Stripe web billing, Neon, and R2 where the scaffolded helper files indicate.
|
|
176
|
+
4. Use \`stack.json\` when debugging generated projects; it records the verified dependency manifest used by this init run.
|
|
177
|
+
5. Review \`PRODUCTION.md\` before connecting live provider projects.
|
|
178
|
+
6. Run database generation and migrations from the shared DB package:
|
|
231
179
|
|
|
232
180
|
\`\`\`bash
|
|
233
181
|
pnpm db:generate
|
|
234
182
|
pnpm db:migrate
|
|
235
183
|
\`\`\`
|
|
236
|
-
|
|
184
|
+
|
|
185
|
+
Run deployment readiness checks before pushing:
|
|
186
|
+
|
|
187
|
+
\`\`\`bash
|
|
188
|
+
pnpm verify
|
|
189
|
+
pnpm verify:frontend
|
|
190
|
+
pnpm verify:backend
|
|
191
|
+
pnpm verify:extension
|
|
192
|
+
\`\`\`
|
|
193
|
+
|
|
194
|
+
7. Build or run the extension separately. It is not deployed by Vercel:
|
|
195
|
+
|
|
196
|
+
\`\`\`bash
|
|
197
|
+
pnpm dev:extension
|
|
198
|
+
pnpm extension:zip
|
|
199
|
+
\`\`\`
|
|
200
|
+
`);
|
|
201
|
+
}
|
|
202
|
+
function writeProductionGuide(root, options) {
|
|
203
|
+
writeFile(path.join(root, 'PRODUCTION.md'), `# ${options.displayName} Production Guide
|
|
204
|
+
|
|
205
|
+
## Project Structure
|
|
206
|
+
|
|
207
|
+
The generated app is one pnpm monorepo:
|
|
208
|
+
|
|
209
|
+
\`\`\`txt
|
|
210
|
+
.
|
|
211
|
+
├─ Frontend/ Expo + React Native Reusables app
|
|
212
|
+
├─ Backend/ Fastify API
|
|
213
|
+
├─ Extension/ WXT Chrome extension
|
|
214
|
+
├─ packages/
|
|
215
|
+
│ ├─ api-client/ shared typed API client
|
|
216
|
+
│ ├─ config/ shared config constants/helpers
|
|
217
|
+
│ ├─ db/ Drizzle schema, Neon client, migrations
|
|
218
|
+
│ └─ types/ shared TypeScript types
|
|
219
|
+
├─ PRODUCTION.md development and deployment guide
|
|
220
|
+
├─ turbo.json workspace build graph
|
|
221
|
+
└─ package.json root scripts
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
## Frontend
|
|
225
|
+
|
|
226
|
+
\`Frontend\` is the single Expo source for:
|
|
227
|
+
|
|
228
|
+
- Web on Vercel
|
|
229
|
+
- iOS through EAS
|
|
230
|
+
- Android through EAS
|
|
231
|
+
|
|
232
|
+
Important files:
|
|
233
|
+
|
|
234
|
+
- \`Frontend/package.json\`: Expo scripts
|
|
235
|
+
- \`Frontend/vercel.json\`: Vercel web export config
|
|
236
|
+
- \`Frontend/eas.json\`: native build profiles
|
|
237
|
+
- \`Frontend/app\`: Expo Router screens
|
|
238
|
+
|
|
239
|
+
Build output goes to \`Frontend/dist\`.
|
|
240
|
+
|
|
241
|
+
## Backend
|
|
242
|
+
|
|
243
|
+
\`Backend\` is the Fastify API.
|
|
244
|
+
|
|
245
|
+
Important files:
|
|
246
|
+
|
|
247
|
+
- \`Backend/src/index.ts\`: Vercel Fastify entrypoint
|
|
248
|
+
- \`Backend/src/app.ts\`: Fastify app construction
|
|
249
|
+
- \`Backend/vercel.json\`: minimal Vercel config
|
|
250
|
+
- \`Backend/src/routes\`: health/auth/subscription routes
|
|
251
|
+
|
|
252
|
+
## Extension
|
|
253
|
+
|
|
254
|
+
\`Extension\` is the WXT Chrome extension using the Side Panel API.
|
|
255
|
+
|
|
256
|
+
Important files:
|
|
257
|
+
|
|
258
|
+
- \`Extension/wxt.config.ts\`: manifest, side panel, permissions
|
|
259
|
+
- \`Extension/src/entrypoints/sidepanel\`: side panel UI
|
|
260
|
+
- \`Extension/src/entrypoints/background.ts\`: side panel behavior
|
|
261
|
+
|
|
262
|
+
ZIP output goes to \`Extension/.output\`.
|
|
263
|
+
|
|
264
|
+
## Develop
|
|
265
|
+
|
|
266
|
+
From the repository root:
|
|
267
|
+
|
|
268
|
+
\`\`\`sh
|
|
269
|
+
pnpm install
|
|
270
|
+
pnpm dev
|
|
271
|
+
\`\`\`
|
|
272
|
+
|
|
273
|
+
Or run one surface at a time:
|
|
274
|
+
|
|
275
|
+
\`\`\`sh
|
|
276
|
+
pnpm dev:frontend
|
|
277
|
+
pnpm dev:backend
|
|
278
|
+
pnpm dev:extension
|
|
279
|
+
\`\`\`
|
|
280
|
+
|
|
281
|
+
Use these before deploy:
|
|
282
|
+
|
|
283
|
+
\`\`\`sh
|
|
284
|
+
pnpm verify
|
|
285
|
+
pnpm verify:frontend
|
|
286
|
+
pnpm verify:backend
|
|
287
|
+
pnpm verify:extension
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
Database workflow:
|
|
291
|
+
|
|
292
|
+
\`\`\`sh
|
|
293
|
+
pnpm db:generate
|
|
294
|
+
pnpm db:migrate
|
|
295
|
+
\`\`\`
|
|
296
|
+
|
|
297
|
+
Local demo mode is enabled through generated \`.env\` files. Real provider behavior needs Clerk, RevenueCat/Stripe, Neon, and R2 credentials.
|
|
298
|
+
|
|
299
|
+
## Environment Variables
|
|
300
|
+
|
|
301
|
+
- Root/local: \`DATABASE_URL\` for shared Drizzle commands.
|
|
302
|
+
- Frontend/Vercel Frontend: only \`EXPO_PUBLIC_*\` values.
|
|
303
|
+
- Backend/Vercel Backend: server secrets such as \`DATABASE_URL\`, \`CLERK_SECRET_KEY\`, \`RC_SECRET_API_KEY\`, \`RC_WEBHOOK_SECRET\`, and R2 credentials.
|
|
304
|
+
- Extension: \`VITE_API_URL\`, \`VITE_CLERK_PUBLISHABLE_KEY\`, and optionally \`VITE_CRX_PUBLIC_KEY\`.
|
|
305
|
+
- EAS: add native app public keys with \`eas secret:create\` or the EAS dashboard.
|
|
306
|
+
|
|
307
|
+
## Deploy
|
|
308
|
+
|
|
309
|
+
Vercel deployment is two Vercel projects from the same Git repo:
|
|
310
|
+
|
|
311
|
+
1. Frontend Vercel project
|
|
312
|
+
- Root Directory: \`Frontend\`
|
|
313
|
+
- Build command: \`pnpm build:web\`
|
|
314
|
+
- Output directory: \`dist\`
|
|
315
|
+
|
|
316
|
+
2. Backend Vercel project
|
|
317
|
+
- Root Directory: \`Backend\`
|
|
318
|
+
- Build command: \`pnpm build\`
|
|
319
|
+
- Fastify entrypoint: \`src/index.ts\`
|
|
320
|
+
|
|
321
|
+
Enable Vercel access to source files outside each project root so \`packages/*\` workspace packages resolve.
|
|
322
|
+
|
|
323
|
+
Native app deployment:
|
|
324
|
+
|
|
325
|
+
\`\`\`sh
|
|
326
|
+
cd Frontend
|
|
327
|
+
pnpm dlx eas-cli@latest login
|
|
328
|
+
pnpm dlx eas-cli@latest init
|
|
329
|
+
pnpm dlx eas-cli@latest build --platform all --profile production
|
|
330
|
+
pnpm dlx eas-cli@latest submit --platform all --latest --profile production
|
|
331
|
+
\`\`\`
|
|
332
|
+
|
|
333
|
+
Chrome extension deployment:
|
|
334
|
+
|
|
335
|
+
\`\`\`sh
|
|
336
|
+
pnpm extension:zip
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
Upload \`Extension/.output/*-chrome.zip\` to the Chrome Web Store Developer Dashboard.
|
|
340
|
+
|
|
341
|
+
## Provider Checklist
|
|
342
|
+
|
|
343
|
+
- Clerk: configure allowed origins for the Expo web Vercel domain, native redirect/deep-link settings for the Expo scheme in \`Frontend/app.json\`, and the Chrome extension origin after publishing or after assigning a stable extension key.
|
|
344
|
+
- RevenueCat + Stripe: create a \`pro\` entitlement, configure iOS/Android/Web app API keys separately, connect Stripe as the web billing source, set the RevenueCat webhook URL to \`https://<backend-domain>/webhooks/revenuecat\`, and keep \`RC_WEBHOOK_SECRET\` only in Backend.
|
|
345
|
+
- Neon + Drizzle: create the Neon database, set \`DATABASE_URL\` locally and in the Backend Vercel project, then run \`pnpm db:generate\` and \`pnpm db:migrate\` before production traffic.
|
|
346
|
+
- Cloudflare R2: create an R2 bucket and least-privilege API token, then set \`R2_ACCOUNT_ID\`, \`R2_ACCESS_KEY_ID\`, \`R2_SECRET_ACCESS_KEY\`, and \`R2_BUCKET\` in Backend only.
|
|
347
|
+
|
|
348
|
+
## Relevant Docs
|
|
349
|
+
|
|
350
|
+
- [Vercel monorepos](https://vercel.com/docs/monorepos)
|
|
351
|
+
- [Expo web publishing](https://docs.expo.dev/guides/publishing-websites/)
|
|
352
|
+
- [Expo EAS](https://docs.expo.dev/eas/)
|
|
353
|
+
- [WXT publishing](https://wxt.dev/guide/essentials/publishing.html)
|
|
354
|
+
- [Chrome Web Store publishing](https://developer.chrome.com/docs/webstore/publish)
|
|
237
355
|
`);
|
|
238
356
|
}
|
|
239
357
|
function writeStackFile(root, stack) {
|
|
@@ -258,17 +376,8 @@ function writeSharedPackages(root) {
|
|
|
258
376
|
'db:generate': 'drizzle-kit generate',
|
|
259
377
|
'db:migrate': 'tsx --env-file=../../.env src/migrate.ts',
|
|
260
378
|
},
|
|
261
|
-
dependencies:
|
|
262
|
-
|
|
263
|
-
'drizzle-orm': '^0.44.0',
|
|
264
|
-
dotenv: '^17.2.3',
|
|
265
|
-
},
|
|
266
|
-
devDependencies: {
|
|
267
|
-
'drizzle-kit': '^0.31.0',
|
|
268
|
-
tsx: '^4.20.5',
|
|
269
|
-
typescript: '^5.9.3',
|
|
270
|
-
'@types/node': '25.5.0',
|
|
271
|
-
},
|
|
379
|
+
dependencies: SHARED_DB_DEPENDENCIES.dependencies,
|
|
380
|
+
devDependencies: SHARED_DB_DEPENDENCIES.devDependencies,
|
|
272
381
|
}, null, 2) + '\n');
|
|
273
382
|
writeFile(path.join(dbRoot, 'tsconfig.json'), JSON.stringify({
|
|
274
383
|
compilerOptions: {
|
|
@@ -375,8 +484,7 @@ export const users = pgTable('users', {
|
|
|
375
484
|
displayName: text('display_name'),
|
|
376
485
|
lastLoginAt: timestamp('last_login_at'),
|
|
377
486
|
profileImageUrl: text('profile_image_url'),
|
|
378
|
-
|
|
379
|
-
subscriptionTier: text('subscription_tier').notNull().default('starter'),
|
|
487
|
+
subscriptionTier: text('subscription_tier').notNull().default('free'),
|
|
380
488
|
subscriptionStatus: text('subscription_status').notNull().default('active'),
|
|
381
489
|
avatarObjectKey: text('avatar_object_key'),
|
|
382
490
|
avatarMimeType: text('avatar_mime_type'),
|
|
@@ -526,17 +634,6 @@ export async function syncUserProfile(input: SyncUserInput) {
|
|
|
526
634
|
return findUserByClerkId(input.clerkUserId);
|
|
527
635
|
}
|
|
528
636
|
|
|
529
|
-
export async function updateStripeCustomerForUser(clerkUserId: string, stripeCustomerId: string) {
|
|
530
|
-
await db
|
|
531
|
-
.update(users)
|
|
532
|
-
.set({
|
|
533
|
-
stripeCustomerId,
|
|
534
|
-
subscriptionStatus: 'checkout_started',
|
|
535
|
-
updatedAt: new Date(),
|
|
536
|
-
})
|
|
537
|
-
.where(eq(users.clerkUserId, clerkUserId));
|
|
538
|
-
}
|
|
539
|
-
|
|
540
637
|
export async function updateAvatarForUser(
|
|
541
638
|
clerkUserId: string,
|
|
542
639
|
input: { objectKey: string; contentType: string | null },
|
|
@@ -600,7 +697,7 @@ console.log('Migrations completed in', Date.now() - start, 'ms');
|
|
|
600
697
|
writeFile(path.join(dbRoot, 'migrations/.gitkeep'), '');
|
|
601
698
|
const simplePackages = [
|
|
602
699
|
['types', { exports: { '.': './src/index.ts' } }, `export type ApiEnvelope<T> = { data: T } | { error: string; message: string };
|
|
603
|
-
export type ClientPlatform = '
|
|
700
|
+
export type ClientPlatform = 'frontend' | 'extension';
|
|
604
701
|
export type SubscriptionEntitlements = {
|
|
605
702
|
pro: boolean;
|
|
606
703
|
inTrial: boolean;
|
|
@@ -618,7 +715,7 @@ export type AuthMeResponse = {
|
|
|
618
715
|
email: string;
|
|
619
716
|
firstName?: string | null;
|
|
620
717
|
lastName?: string | null;
|
|
621
|
-
displayName
|
|
718
|
+
displayName: string;
|
|
622
719
|
imageUrl?: string | null;
|
|
623
720
|
avatarUrl?: string | null;
|
|
624
721
|
creditsBalance: number;
|
|
@@ -627,19 +724,26 @@ export type AuthMeResponse = {
|
|
|
627
724
|
};
|
|
628
725
|
};
|
|
629
726
|
export type PricingResponse = {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
727
|
+
tiers: Array<{
|
|
728
|
+
tier: 'free' | 'pro';
|
|
729
|
+
displayName: string;
|
|
633
730
|
description: string;
|
|
634
|
-
|
|
635
|
-
|
|
731
|
+
priceMonthly: number | null;
|
|
732
|
+
priceYearly: number | null;
|
|
733
|
+
currency: string;
|
|
734
|
+
limits: {
|
|
735
|
+
dailyLimit: number | null;
|
|
736
|
+
};
|
|
737
|
+
paymentInfo?: {
|
|
738
|
+
revenueCatEntitlementId: string;
|
|
739
|
+
revenueCatOfferingId: string;
|
|
740
|
+
};
|
|
636
741
|
}>;
|
|
637
742
|
};
|
|
638
743
|
`],
|
|
639
744
|
['config', { exports: { '.': './src/index.ts' } }, `export const DEFAULT_API_PATH_PREFIX = '/api';
|
|
640
745
|
export const DEFAULT_LOCAL_API_URL = 'http://localhost:8787';
|
|
641
|
-
export const
|
|
642
|
-
export const DEFAULT_MOBILE_URL = 'http://localhost:8081';
|
|
746
|
+
export const DEFAULT_FRONTEND_URL = 'http://localhost:8081';
|
|
643
747
|
export const DEFAULT_EXTENSION_URL = 'chrome-extension://';
|
|
644
748
|
|
|
645
749
|
export function joinApiUrl(baseUrl: string, path: string) {
|
|
@@ -650,13 +754,21 @@ export function joinApiUrl(baseUrl: string, path: string) {
|
|
|
650
754
|
`],
|
|
651
755
|
['api-client', {
|
|
652
756
|
exports: { '.': './src/index.ts' },
|
|
653
|
-
dependencies:
|
|
654
|
-
'@anhedral/config': 'workspace:*',
|
|
655
|
-
'@anhedral/types': 'workspace:*',
|
|
656
|
-
},
|
|
757
|
+
dependencies: API_CLIENT_DEPENDENCIES.dependencies,
|
|
657
758
|
}, `import { joinApiUrl } from '@anhedral/config';
|
|
658
759
|
import type { AuthMeResponse, ClientPlatform, PricingResponse, SubscriptionEntitlements } from '@anhedral/types';
|
|
659
760
|
|
|
761
|
+
export class APIRequestError extends Error {
|
|
762
|
+
constructor(
|
|
763
|
+
public statusCode: number,
|
|
764
|
+
message: string,
|
|
765
|
+
public errorCode?: string,
|
|
766
|
+
) {
|
|
767
|
+
super(message);
|
|
768
|
+
this.name = 'APIRequestError';
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
660
772
|
export type ApiClientOptions = {
|
|
661
773
|
baseUrl: string;
|
|
662
774
|
platform: ClientPlatform;
|
|
@@ -679,9 +791,16 @@ export class ApiClient {
|
|
|
679
791
|
});
|
|
680
792
|
|
|
681
793
|
if (!response.ok) {
|
|
682
|
-
|
|
794
|
+
let error: { error?: string; message?: string } = {};
|
|
795
|
+
try { error = await response.json(); } catch {}
|
|
796
|
+
throw new APIRequestError(
|
|
797
|
+
response.status,
|
|
798
|
+
error.message || \`API request failed: \${response.status}\`,
|
|
799
|
+
error.error,
|
|
800
|
+
);
|
|
683
801
|
}
|
|
684
802
|
|
|
803
|
+
if (response.status === 204) return {} as T;
|
|
685
804
|
return response.json() as Promise<T>;
|
|
686
805
|
}
|
|
687
806
|
|
|
@@ -693,6 +812,10 @@ export class ApiClient {
|
|
|
693
812
|
return this.request<PricingResponse>('/subscriptions/pricing');
|
|
694
813
|
}
|
|
695
814
|
|
|
815
|
+
getSubscriptionPricing() {
|
|
816
|
+
return this.getPricing();
|
|
817
|
+
}
|
|
818
|
+
|
|
696
819
|
getSubscriptionEntitlements(options?: { refresh?: boolean }) {
|
|
697
820
|
return this.request<SubscriptionEntitlements>(
|
|
698
821
|
options?.refresh ? '/subscriptions/entitlements/me?refresh=true' : '/subscriptions/entitlements/me',
|
|
@@ -705,6 +828,25 @@ export class ApiClient {
|
|
|
705
828
|
body: JSON.stringify({ code }),
|
|
706
829
|
});
|
|
707
830
|
}
|
|
831
|
+
|
|
832
|
+
signOut() {
|
|
833
|
+
return this.request<{ success: boolean }>('/auth/signout', {
|
|
834
|
+
method: 'POST',
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
uploadAvatar(input: { base64: string; mimeType: string; fileName?: string }) {
|
|
839
|
+
return this.request<{ ok: boolean; avatarUrl: string }>('/auth/avatar', {
|
|
840
|
+
method: 'POST',
|
|
841
|
+
body: JSON.stringify(input),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
deleteAccount() {
|
|
846
|
+
return this.request<void>('/auth/account', {
|
|
847
|
+
method: 'DELETE',
|
|
848
|
+
});
|
|
849
|
+
}
|
|
708
850
|
}
|
|
709
851
|
`],
|
|
710
852
|
];
|
|
@@ -719,9 +861,7 @@ export class ApiClient {
|
|
|
719
861
|
build: 'pnpm typecheck',
|
|
720
862
|
typecheck: 'tsc --noEmit',
|
|
721
863
|
},
|
|
722
|
-
devDependencies:
|
|
723
|
-
typescript: '^5.9.3',
|
|
724
|
-
},
|
|
864
|
+
devDependencies: SHARED_PACKAGE_DEPENDENCIES.devDependencies,
|
|
725
865
|
...packageFields,
|
|
726
866
|
}, null, 2) + '\n');
|
|
727
867
|
writeFile(path.join(packageRoot, 'tsconfig.json'), JSON.stringify({
|
|
@@ -738,54 +878,31 @@ export class ApiClient {
|
|
|
738
878
|
writeFile(path.join(packageRoot, 'src/index.ts'), source);
|
|
739
879
|
}
|
|
740
880
|
}
|
|
741
|
-
function writeRootEnvExample(root
|
|
742
|
-
const frontendUrl =
|
|
743
|
-
const
|
|
744
|
-
? `NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
745
|
-
NEXT_PUBLIC_API_URL=http://localhost:8787
|
|
746
|
-
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
747
|
-
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
|
748
|
-
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
|
749
|
-
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
|
|
750
|
-
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
|
|
751
|
-
`
|
|
752
|
-
: '';
|
|
753
|
-
const expoEnv = options.frontend === 'expo'
|
|
754
|
-
? `EXPO_PUBLIC_API_URL=http://localhost:8787
|
|
881
|
+
function writeRootEnvExample(root) {
|
|
882
|
+
const frontendUrl = 'http://localhost:8081';
|
|
883
|
+
const expoEnv = `EXPO_PUBLIC_API_URL=http://localhost:8787
|
|
755
884
|
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
756
885
|
EXPO_PUBLIC_RC_ENTITLEMENT_ID=pro
|
|
757
886
|
EXPO_PUBLIC_RC_API_KEY_IOS=
|
|
758
887
|
EXPO_PUBLIC_RC_API_KEY_ANDROID=
|
|
759
888
|
EXPO_PUBLIC_RC_WEB_API_KEY=
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const extensionEnv = options.extension
|
|
763
|
-
? `VITE_API_URL=http://localhost:8787
|
|
889
|
+
`;
|
|
890
|
+
const extensionEnv = `VITE_API_URL=http://localhost:8787
|
|
764
891
|
VITE_WEBSITE_URL=${frontendUrl}
|
|
765
892
|
VITE_CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
766
893
|
VITE_CRX_PUBLIC_KEY=
|
|
767
894
|
VITE_RC_BILLING_URL=
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const revenueCatEnv = options.payments === 'revenuecat'
|
|
771
|
-
? `# RevenueCat
|
|
895
|
+
`;
|
|
896
|
+
const paymentsEnv = `# RevenueCat
|
|
772
897
|
RC_SECRET_API_KEY=
|
|
773
898
|
RC_WEBHOOK_SECRET=
|
|
774
899
|
RC_ENTITLEMENT_ID=pro
|
|
775
900
|
RC_OFFERING_ID=default
|
|
776
|
-
|
|
777
|
-
: '';
|
|
778
|
-
const stripeEnv = options.payments === 'stripe'
|
|
779
|
-
? `# Stripe
|
|
780
|
-
STRIPE_SECRET_KEY=sk_test_***
|
|
781
|
-
STRIPE_WEBHOOK_SECRET=whsec_***
|
|
782
|
-
STRIPE_PRICE_STARTER=price_***
|
|
783
|
-
`
|
|
784
|
-
: '';
|
|
901
|
+
`;
|
|
785
902
|
writeFile(path.join(root, '.env.example'), `# Apps
|
|
786
903
|
FRONTEND_URL=${frontendUrl}
|
|
787
904
|
ANHEDRAL_DEMO=false
|
|
788
|
-
${
|
|
905
|
+
${expoEnv}${extensionEnv}
|
|
789
906
|
|
|
790
907
|
# Server
|
|
791
908
|
PORT=8787
|
|
@@ -801,126 +918,129 @@ R2_ACCESS_KEY_ID=
|
|
|
801
918
|
R2_SECRET_ACCESS_KEY=
|
|
802
919
|
R2_BUCKET=
|
|
803
920
|
|
|
804
|
-
${
|
|
921
|
+
${paymentsEnv}`);
|
|
805
922
|
}
|
|
806
|
-
function
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
{ src: 'apps/api/api/index.ts', use: '@vercel/node' },
|
|
811
|
-
]
|
|
812
|
-
: [
|
|
813
|
-
{ src: 'apps/api/api/index.ts', use: '@vercel/node' },
|
|
814
|
-
];
|
|
815
|
-
const routes = options.frontend === 'next'
|
|
816
|
-
? [
|
|
817
|
-
{ src: '/backend/(.*)', dest: 'apps/api/api/index.ts' },
|
|
818
|
-
{ src: '/(.*)', dest: 'apps/web/$1' },
|
|
819
|
-
]
|
|
820
|
-
: [
|
|
821
|
-
{ src: '/api/(.*)', dest: 'apps/api/api/index.ts' },
|
|
822
|
-
{ src: '/(.*)', dest: 'apps/api/api/index.ts' },
|
|
823
|
-
];
|
|
824
|
-
writeFile(path.join(root, 'vercel.json'), JSON.stringify({ version: 2, builds, routes }, null, 2) + '\n');
|
|
825
|
-
writeFile(path.join(root, '.vercelignore'), `apps/extension/.output
|
|
826
|
-
apps/extension/.wxt
|
|
827
|
-
apps/extension/dist
|
|
828
|
-
apps/mobile/.expo
|
|
923
|
+
function writeRootVercelFiles(root) {
|
|
924
|
+
writeFile(path.join(root, '.vercelignore'), `Extension/.output
|
|
925
|
+
Extension/.wxt
|
|
926
|
+
Extension/dist
|
|
829
927
|
`);
|
|
830
928
|
}
|
|
831
|
-
function
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
929
|
+
function writeGeneratedCiWorkflow(root) {
|
|
930
|
+
writeFile(path.join(root, '.github/workflows/ci.yml'), `name: CI
|
|
931
|
+
|
|
932
|
+
on:
|
|
933
|
+
pull_request:
|
|
934
|
+
push:
|
|
935
|
+
branches:
|
|
936
|
+
- main
|
|
937
|
+
|
|
938
|
+
permissions:
|
|
939
|
+
contents: read
|
|
940
|
+
|
|
941
|
+
concurrency:
|
|
942
|
+
group: ci-\${{ github.workflow }}-\${{ github.ref }}
|
|
943
|
+
cancel-in-progress: true
|
|
944
|
+
|
|
945
|
+
jobs:
|
|
946
|
+
verify:
|
|
947
|
+
runs-on: ubuntu-latest
|
|
948
|
+
timeout-minutes: 45
|
|
949
|
+
steps:
|
|
950
|
+
- name: Checkout
|
|
951
|
+
uses: actions/checkout@v4
|
|
952
|
+
|
|
953
|
+
- name: Setup pnpm
|
|
954
|
+
uses: pnpm/action-setup@v4
|
|
955
|
+
with:
|
|
956
|
+
version: 10.15.1
|
|
957
|
+
|
|
958
|
+
- name: Setup Node.js
|
|
959
|
+
uses: actions/setup-node@v4
|
|
960
|
+
with:
|
|
961
|
+
node-version: 20
|
|
962
|
+
cache: pnpm
|
|
963
|
+
|
|
964
|
+
- name: Install dependencies
|
|
965
|
+
run: pnpm install --frozen-lockfile
|
|
966
|
+
|
|
967
|
+
- name: Check Expo dependency alignment
|
|
968
|
+
run: pnpm --filter ./Frontend exec expo install --check
|
|
969
|
+
|
|
970
|
+
- name: Build Expo web
|
|
971
|
+
run: pnpm --filter ./Frontend build:web
|
|
972
|
+
|
|
973
|
+
- name: Test API
|
|
974
|
+
run: pnpm --filter ./Backend test
|
|
975
|
+
|
|
976
|
+
- name: Build API
|
|
977
|
+
run: pnpm --filter ./Backend build
|
|
978
|
+
|
|
979
|
+
- name: Build extension
|
|
980
|
+
run: pnpm --filter ./Extension build
|
|
981
|
+
|
|
982
|
+
- name: Zip extension
|
|
983
|
+
run: pnpm --filter ./Extension zip
|
|
984
|
+
|
|
985
|
+
- name: Build workspace
|
|
986
|
+
run: pnpm build
|
|
987
|
+
`);
|
|
886
988
|
}
|
|
887
989
|
function writeFullstackRootFiles(root, options) {
|
|
888
990
|
const appFilters = [
|
|
889
|
-
|
|
890
|
-
'./
|
|
891
|
-
|
|
991
|
+
'./Frontend',
|
|
992
|
+
'./Backend',
|
|
993
|
+
'./Extension',
|
|
892
994
|
];
|
|
893
|
-
const parallelFilters = appFilters.map((entry) => `--filter
|
|
894
|
-
const buildFilters = ['./packages/db', ...appFilters].map((entry) => `--filter ${entry}`).join(' ');
|
|
995
|
+
const parallelFilters = appFilters.map((entry) => `--filter=${entry}`).join(' ');
|
|
895
996
|
const scripts = {
|
|
896
|
-
dev: `
|
|
897
|
-
'dev:
|
|
898
|
-
build:
|
|
899
|
-
typecheck: '
|
|
997
|
+
dev: `turbo dev --parallel ${parallelFilters}`,
|
|
998
|
+
'dev:backend': 'pnpm --filter ./Backend dev',
|
|
999
|
+
build: 'turbo build',
|
|
1000
|
+
typecheck: 'turbo typecheck',
|
|
1001
|
+
verify: 'pnpm verify:frontend && pnpm verify:backend && pnpm verify:extension',
|
|
1002
|
+
'verify:frontend': 'pnpm --filter ./Frontend exec expo install --check && pnpm --filter ./Frontend build:web',
|
|
1003
|
+
'verify:backend': 'pnpm --filter ./Backend test && pnpm --filter ./Backend build',
|
|
1004
|
+
'verify:extension': 'pnpm --filter ./Extension typecheck && pnpm --filter ./Extension zip',
|
|
900
1005
|
'db:generate': 'pnpm --filter @anhedral/db db:generate',
|
|
901
1006
|
'db:migrate': 'pnpm --filter @anhedral/db db:migrate',
|
|
902
1007
|
};
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1008
|
+
scripts['dev:frontend'] = 'pnpm --filter ./Frontend dev';
|
|
1009
|
+
scripts['dev:extension'] = 'pnpm --filter ./Extension dev';
|
|
1010
|
+
scripts['extension:zip'] = 'pnpm --filter ./Extension zip';
|
|
1011
|
+
writeRootPackageJson(root, options.projectName, scripts, ['Frontend', 'Backend', 'Extension', 'packages/*'], {
|
|
1012
|
+
devDependencies: ROOT_DEPENDENCIES.devDependencies,
|
|
1013
|
+
});
|
|
1014
|
+
writePnpmWorkspace(root, ['Frontend', 'Backend', 'Extension', 'packages/*']);
|
|
1015
|
+
writeFile(path.join(root, 'turbo.json'), JSON.stringify({
|
|
1016
|
+
$schema: 'https://turborepo.dev/schema.json',
|
|
1017
|
+
tasks: {
|
|
1018
|
+
build: {
|
|
1019
|
+
dependsOn: ['^build'],
|
|
1020
|
+
outputs: [],
|
|
1021
|
+
},
|
|
1022
|
+
typecheck: {
|
|
1023
|
+
dependsOn: ['^build'],
|
|
1024
|
+
},
|
|
1025
|
+
dev: {
|
|
1026
|
+
cache: false,
|
|
1027
|
+
persistent: true,
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
}, null, 2) + '\n');
|
|
915
1031
|
appendGitignore(root, [
|
|
916
1032
|
'node_modules',
|
|
917
|
-
'
|
|
1033
|
+
'.turbo',
|
|
1034
|
+
'Frontend/node_modules',
|
|
1035
|
+
'Backend/node_modules',
|
|
1036
|
+
'Extension/node_modules',
|
|
918
1037
|
'packages/*/node_modules',
|
|
919
1038
|
...SHARED_ENV_GITIGNORE_LINES,
|
|
920
1039
|
...SHARED_TYPESCRIPT_GITIGNORE_LINES,
|
|
921
1040
|
]);
|
|
922
|
-
writeRootEnvExample(root
|
|
923
|
-
|
|
1041
|
+
writeRootEnvExample(root);
|
|
1042
|
+
writeRootVercelFiles(root);
|
|
1043
|
+
writeGeneratedCiWorkflow(root);
|
|
924
1044
|
}
|
|
925
1045
|
async function scaffoldFullstack(root, options) {
|
|
926
1046
|
anhedralPrint.section('Workspace root');
|
|
@@ -931,36 +1051,34 @@ async function scaffoldFullstack(root, options) {
|
|
|
931
1051
|
anhedralPrint.step('Writing @anhedral/db, types, config, api-client');
|
|
932
1052
|
writeSharedPackages(root);
|
|
933
1053
|
anhedralPrint.done('Shared packages written');
|
|
934
|
-
const frontendUrl =
|
|
1054
|
+
const frontendUrl = 'http://localhost:8081';
|
|
935
1055
|
await scaffoldBackend(root, {
|
|
936
1056
|
projectName: options.projectName,
|
|
937
1057
|
displayName: options.displayName,
|
|
938
1058
|
githubOrg: null,
|
|
939
1059
|
frontendUrl,
|
|
1060
|
+
skipInstall: options.skipInstall,
|
|
1061
|
+
});
|
|
1062
|
+
await scaffoldFrontend(root, {
|
|
1063
|
+
projectName: options.projectName,
|
|
1064
|
+
displayName: options.displayName,
|
|
1065
|
+
githubOrg: null,
|
|
1066
|
+
frontendUrl,
|
|
1067
|
+
skipInstall: options.skipInstall,
|
|
1068
|
+
});
|
|
1069
|
+
await scaffoldExtension(root, {
|
|
1070
|
+
projectName: options.projectName,
|
|
1071
|
+
displayName: options.displayName,
|
|
1072
|
+
githubOrg: null,
|
|
1073
|
+
frontendUrl,
|
|
1074
|
+
skipInstall: options.skipInstall,
|
|
940
1075
|
});
|
|
941
|
-
if (options.frontend === 'next') {
|
|
942
|
-
scaffoldNextFrontend(root, options, 'web');
|
|
943
|
-
}
|
|
944
|
-
else {
|
|
945
|
-
await scaffoldFrontend(root, {
|
|
946
|
-
projectName: options.projectName,
|
|
947
|
-
displayName: options.displayName,
|
|
948
|
-
githubOrg: null,
|
|
949
|
-
frontendUrl,
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
if (options.extension) {
|
|
953
|
-
await scaffoldExtension(root, {
|
|
954
|
-
projectName: options.projectName,
|
|
955
|
-
displayName: options.displayName,
|
|
956
|
-
githubOrg: null,
|
|
957
|
-
frontendUrl,
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
1076
|
return [
|
|
961
|
-
|
|
962
|
-
'
|
|
963
|
-
|
|
1077
|
+
'.github/workflows/ci.yml',
|
|
1078
|
+
'PRODUCTION.md',
|
|
1079
|
+
'Frontend',
|
|
1080
|
+
'Backend',
|
|
1081
|
+
'Extension',
|
|
964
1082
|
'packages/db',
|
|
965
1083
|
'packages/types',
|
|
966
1084
|
'packages/config',
|
|
@@ -978,10 +1096,11 @@ export async function scaffoldProject(options) {
|
|
|
978
1096
|
const generatedPaths = await scaffoldFullstack(root, options);
|
|
979
1097
|
anhedralPrint.section('Project metadata');
|
|
980
1098
|
anhedralPrint.step('Writing skills guide, README, and stack.json');
|
|
981
|
-
const skillCommands = getSkillCommands(
|
|
1099
|
+
const skillCommands = getSkillCommands();
|
|
982
1100
|
writeSkillsGuide(root, skillCommands);
|
|
983
1101
|
const stack = normalizeStack(options, generatedPaths);
|
|
984
1102
|
writeRootDocs(root, options, stack);
|
|
1103
|
+
writeProductionGuide(root, options);
|
|
985
1104
|
writeStackFile(root, stack);
|
|
986
1105
|
anhedralPrint.done('Project metadata written');
|
|
987
1106
|
console.log('');
|
|
@@ -993,18 +1112,12 @@ export async function scaffoldProject(options) {
|
|
|
993
1112
|
anhedralPrint.info('Next commands:');
|
|
994
1113
|
console.log(' pnpm install');
|
|
995
1114
|
console.log(' cp .env.example .env');
|
|
996
|
-
|
|
997
|
-
console.log(' cp apps/web/.env.example apps/web/.env.local');
|
|
998
|
-
}
|
|
999
|
-
if (options.extension) {
|
|
1000
|
-
console.log(' cp apps/extension/.env.example apps/extension/.env');
|
|
1001
|
-
}
|
|
1115
|
+
console.log(' cp Extension/.env.example Extension/.env');
|
|
1002
1116
|
console.log(' pnpm db:generate && pnpm db:migrate');
|
|
1003
|
-
console.log(' pnpm
|
|
1004
|
-
console.log(
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1117
|
+
console.log(' pnpm verify');
|
|
1118
|
+
console.log(' pnpm dev:backend');
|
|
1119
|
+
console.log(' pnpm dev:frontend');
|
|
1120
|
+
console.log(' pnpm dev:extension');
|
|
1008
1121
|
console.log('');
|
|
1009
1122
|
}
|
|
1010
1123
|
finally {
|