anhedral 0.1.3 → 0.1.4
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 +126 -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 +175 -0
- package/dist/scaffold.d.ts +6 -7
- package/dist/scaffold.js +223 -285
- package/dist/templates/backend.d.ts +1 -1
- package/dist/templates/backend.js +46 -10
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +74 -58
- package/dist/templates/frontend.d.ts +1 -1
- package/dist/templates/frontend.js +83 -171
- 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,11 +147,13 @@ ${generatedPaths}
|
|
|
205
147
|
\`\`\`bash
|
|
206
148
|
pnpm install
|
|
207
149
|
cp .env.example .env
|
|
208
|
-
|
|
150
|
+
# apps/frontend/.env is generated for local Expo development
|
|
151
|
+
cp apps/extension/.env.example apps/extension/.env
|
|
209
152
|
pnpm db:generate
|
|
210
153
|
pnpm db:migrate
|
|
211
154
|
pnpm dev:api
|
|
212
|
-
|
|
155
|
+
pnpm dev:frontend
|
|
156
|
+
pnpm dev:extension
|
|
213
157
|
\`\`\`
|
|
214
158
|
|
|
215
159
|
For a provider-free smoke test, keep \`ANHEDRAL_DEMO=true\` in \`apps/api/.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.
|
|
@@ -217,23 +161,31 @@ For a provider-free smoke test, keep \`ANHEDRAL_DEMO=true\` in \`apps/api/.env\`
|
|
|
217
161
|
## Provider Setup
|
|
218
162
|
|
|
219
163
|
- 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
|
-
|
|
164
|
+
- Clerk: create an application, configure allowed origins, and set \`CLERK_PUBLISHABLE_KEY\`, \`CLERK_SECRET_KEY\`, plus the Expo and extension publishable keys.
|
|
165
|
+
- 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
166
|
- 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
|
-
|
|
167
|
+
- Vercel: import this same Git repository twice. Use \`apps/frontend\` as the root directory for the Expo web project, and \`apps/api\` 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.
|
|
168
|
+
- EAS: use \`apps/frontend\` as the Expo project root for iOS and Android builds. Vercel deploys the web export from the same source.
|
|
224
169
|
|
|
225
170
|
## Setup Notes
|
|
226
171
|
|
|
227
172
|
1. Copy \`.env.example\` into the runtime env files your apps need and fill in provider values.
|
|
228
173
|
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.
|
|
174
|
+
3. Add the real provider configuration for Clerk, RevenueCat, Stripe web billing, Neon, and R2 where the scaffolded helper files indicate.
|
|
175
|
+
4. Use \`stack.json\` when debugging generated projects; it records the verified dependency manifest used by this init run.
|
|
176
|
+
5. Run database generation and migrations from the shared DB package:
|
|
231
177
|
|
|
232
178
|
\`\`\`bash
|
|
233
179
|
pnpm db:generate
|
|
234
180
|
pnpm db:migrate
|
|
235
181
|
\`\`\`
|
|
236
|
-
|
|
182
|
+
|
|
183
|
+
6. Build or run the extension separately. It is not deployed by Vercel:
|
|
184
|
+
|
|
185
|
+
\`\`\`bash
|
|
186
|
+
pnpm dev:extension
|
|
187
|
+
pnpm extension:zip
|
|
188
|
+
\`\`\`
|
|
237
189
|
`);
|
|
238
190
|
}
|
|
239
191
|
function writeStackFile(root, stack) {
|
|
@@ -258,17 +210,8 @@ function writeSharedPackages(root) {
|
|
|
258
210
|
'db:generate': 'drizzle-kit generate',
|
|
259
211
|
'db:migrate': 'tsx --env-file=../../.env src/migrate.ts',
|
|
260
212
|
},
|
|
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
|
-
},
|
|
213
|
+
dependencies: SHARED_DB_DEPENDENCIES.dependencies,
|
|
214
|
+
devDependencies: SHARED_DB_DEPENDENCIES.devDependencies,
|
|
272
215
|
}, null, 2) + '\n');
|
|
273
216
|
writeFile(path.join(dbRoot, 'tsconfig.json'), JSON.stringify({
|
|
274
217
|
compilerOptions: {
|
|
@@ -375,8 +318,7 @@ export const users = pgTable('users', {
|
|
|
375
318
|
displayName: text('display_name'),
|
|
376
319
|
lastLoginAt: timestamp('last_login_at'),
|
|
377
320
|
profileImageUrl: text('profile_image_url'),
|
|
378
|
-
|
|
379
|
-
subscriptionTier: text('subscription_tier').notNull().default('starter'),
|
|
321
|
+
subscriptionTier: text('subscription_tier').notNull().default('free'),
|
|
380
322
|
subscriptionStatus: text('subscription_status').notNull().default('active'),
|
|
381
323
|
avatarObjectKey: text('avatar_object_key'),
|
|
382
324
|
avatarMimeType: text('avatar_mime_type'),
|
|
@@ -526,17 +468,6 @@ export async function syncUserProfile(input: SyncUserInput) {
|
|
|
526
468
|
return findUserByClerkId(input.clerkUserId);
|
|
527
469
|
}
|
|
528
470
|
|
|
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
471
|
export async function updateAvatarForUser(
|
|
541
472
|
clerkUserId: string,
|
|
542
473
|
input: { objectKey: string; contentType: string | null },
|
|
@@ -600,7 +531,7 @@ console.log('Migrations completed in', Date.now() - start, 'ms');
|
|
|
600
531
|
writeFile(path.join(dbRoot, 'migrations/.gitkeep'), '');
|
|
601
532
|
const simplePackages = [
|
|
602
533
|
['types', { exports: { '.': './src/index.ts' } }, `export type ApiEnvelope<T> = { data: T } | { error: string; message: string };
|
|
603
|
-
export type ClientPlatform = '
|
|
534
|
+
export type ClientPlatform = 'frontend' | 'extension';
|
|
604
535
|
export type SubscriptionEntitlements = {
|
|
605
536
|
pro: boolean;
|
|
606
537
|
inTrial: boolean;
|
|
@@ -618,7 +549,7 @@ export type AuthMeResponse = {
|
|
|
618
549
|
email: string;
|
|
619
550
|
firstName?: string | null;
|
|
620
551
|
lastName?: string | null;
|
|
621
|
-
displayName
|
|
552
|
+
displayName: string;
|
|
622
553
|
imageUrl?: string | null;
|
|
623
554
|
avatarUrl?: string | null;
|
|
624
555
|
creditsBalance: number;
|
|
@@ -627,19 +558,26 @@ export type AuthMeResponse = {
|
|
|
627
558
|
};
|
|
628
559
|
};
|
|
629
560
|
export type PricingResponse = {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
561
|
+
tiers: Array<{
|
|
562
|
+
tier: 'free' | 'pro';
|
|
563
|
+
displayName: string;
|
|
633
564
|
description: string;
|
|
634
|
-
|
|
635
|
-
|
|
565
|
+
priceMonthly: number | null;
|
|
566
|
+
priceYearly: number | null;
|
|
567
|
+
currency: string;
|
|
568
|
+
limits: {
|
|
569
|
+
dailyLimit: number | null;
|
|
570
|
+
};
|
|
571
|
+
paymentInfo?: {
|
|
572
|
+
revenueCatEntitlementId: string;
|
|
573
|
+
revenueCatOfferingId: string;
|
|
574
|
+
};
|
|
636
575
|
}>;
|
|
637
576
|
};
|
|
638
577
|
`],
|
|
639
578
|
['config', { exports: { '.': './src/index.ts' } }, `export const DEFAULT_API_PATH_PREFIX = '/api';
|
|
640
579
|
export const DEFAULT_LOCAL_API_URL = 'http://localhost:8787';
|
|
641
|
-
export const
|
|
642
|
-
export const DEFAULT_MOBILE_URL = 'http://localhost:8081';
|
|
580
|
+
export const DEFAULT_FRONTEND_URL = 'http://localhost:8081';
|
|
643
581
|
export const DEFAULT_EXTENSION_URL = 'chrome-extension://';
|
|
644
582
|
|
|
645
583
|
export function joinApiUrl(baseUrl: string, path: string) {
|
|
@@ -650,13 +588,21 @@ export function joinApiUrl(baseUrl: string, path: string) {
|
|
|
650
588
|
`],
|
|
651
589
|
['api-client', {
|
|
652
590
|
exports: { '.': './src/index.ts' },
|
|
653
|
-
dependencies:
|
|
654
|
-
'@anhedral/config': 'workspace:*',
|
|
655
|
-
'@anhedral/types': 'workspace:*',
|
|
656
|
-
},
|
|
591
|
+
dependencies: API_CLIENT_DEPENDENCIES.dependencies,
|
|
657
592
|
}, `import { joinApiUrl } from '@anhedral/config';
|
|
658
593
|
import type { AuthMeResponse, ClientPlatform, PricingResponse, SubscriptionEntitlements } from '@anhedral/types';
|
|
659
594
|
|
|
595
|
+
export class APIRequestError extends Error {
|
|
596
|
+
constructor(
|
|
597
|
+
public statusCode: number,
|
|
598
|
+
message: string,
|
|
599
|
+
public errorCode?: string,
|
|
600
|
+
) {
|
|
601
|
+
super(message);
|
|
602
|
+
this.name = 'APIRequestError';
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
660
606
|
export type ApiClientOptions = {
|
|
661
607
|
baseUrl: string;
|
|
662
608
|
platform: ClientPlatform;
|
|
@@ -679,9 +625,16 @@ export class ApiClient {
|
|
|
679
625
|
});
|
|
680
626
|
|
|
681
627
|
if (!response.ok) {
|
|
682
|
-
|
|
628
|
+
let error: { error?: string; message?: string } = {};
|
|
629
|
+
try { error = await response.json(); } catch {}
|
|
630
|
+
throw new APIRequestError(
|
|
631
|
+
response.status,
|
|
632
|
+
error.message || \`API request failed: \${response.status}\`,
|
|
633
|
+
error.error,
|
|
634
|
+
);
|
|
683
635
|
}
|
|
684
636
|
|
|
637
|
+
if (response.status === 204) return {} as T;
|
|
685
638
|
return response.json() as Promise<T>;
|
|
686
639
|
}
|
|
687
640
|
|
|
@@ -693,6 +646,10 @@ export class ApiClient {
|
|
|
693
646
|
return this.request<PricingResponse>('/subscriptions/pricing');
|
|
694
647
|
}
|
|
695
648
|
|
|
649
|
+
getSubscriptionPricing() {
|
|
650
|
+
return this.getPricing();
|
|
651
|
+
}
|
|
652
|
+
|
|
696
653
|
getSubscriptionEntitlements(options?: { refresh?: boolean }) {
|
|
697
654
|
return this.request<SubscriptionEntitlements>(
|
|
698
655
|
options?.refresh ? '/subscriptions/entitlements/me?refresh=true' : '/subscriptions/entitlements/me',
|
|
@@ -705,6 +662,25 @@ export class ApiClient {
|
|
|
705
662
|
body: JSON.stringify({ code }),
|
|
706
663
|
});
|
|
707
664
|
}
|
|
665
|
+
|
|
666
|
+
signOut() {
|
|
667
|
+
return this.request<{ success: boolean }>('/auth/signout', {
|
|
668
|
+
method: 'POST',
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
uploadAvatar(input: { base64: string; mimeType: string; fileName?: string }) {
|
|
673
|
+
return this.request<{ ok: boolean; avatarUrl: string }>('/auth/avatar', {
|
|
674
|
+
method: 'POST',
|
|
675
|
+
body: JSON.stringify(input),
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
deleteAccount() {
|
|
680
|
+
return this.request<void>('/auth/account', {
|
|
681
|
+
method: 'DELETE',
|
|
682
|
+
});
|
|
683
|
+
}
|
|
708
684
|
}
|
|
709
685
|
`],
|
|
710
686
|
];
|
|
@@ -719,9 +695,7 @@ export class ApiClient {
|
|
|
719
695
|
build: 'pnpm typecheck',
|
|
720
696
|
typecheck: 'tsc --noEmit',
|
|
721
697
|
},
|
|
722
|
-
devDependencies:
|
|
723
|
-
typescript: '^5.9.3',
|
|
724
|
-
},
|
|
698
|
+
devDependencies: SHARED_PACKAGE_DEPENDENCIES.devDependencies,
|
|
725
699
|
...packageFields,
|
|
726
700
|
}, null, 2) + '\n');
|
|
727
701
|
writeFile(path.join(packageRoot, 'tsconfig.json'), JSON.stringify({
|
|
@@ -738,54 +712,31 @@ export class ApiClient {
|
|
|
738
712
|
writeFile(path.join(packageRoot, 'src/index.ts'), source);
|
|
739
713
|
}
|
|
740
714
|
}
|
|
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
|
|
715
|
+
function writeRootEnvExample(root) {
|
|
716
|
+
const frontendUrl = 'http://localhost:8081';
|
|
717
|
+
const expoEnv = `EXPO_PUBLIC_API_URL=http://localhost:8787
|
|
755
718
|
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
756
719
|
EXPO_PUBLIC_RC_ENTITLEMENT_ID=pro
|
|
757
720
|
EXPO_PUBLIC_RC_API_KEY_IOS=
|
|
758
721
|
EXPO_PUBLIC_RC_API_KEY_ANDROID=
|
|
759
722
|
EXPO_PUBLIC_RC_WEB_API_KEY=
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const extensionEnv = options.extension
|
|
763
|
-
? `VITE_API_URL=http://localhost:8787
|
|
723
|
+
`;
|
|
724
|
+
const extensionEnv = `VITE_API_URL=http://localhost:8787
|
|
764
725
|
VITE_WEBSITE_URL=${frontendUrl}
|
|
765
726
|
VITE_CLERK_PUBLISHABLE_KEY=pk_test_***
|
|
766
727
|
VITE_CRX_PUBLIC_KEY=
|
|
767
728
|
VITE_RC_BILLING_URL=
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const revenueCatEnv = options.payments === 'revenuecat'
|
|
771
|
-
? `# RevenueCat
|
|
729
|
+
`;
|
|
730
|
+
const paymentsEnv = `# RevenueCat
|
|
772
731
|
RC_SECRET_API_KEY=
|
|
773
732
|
RC_WEBHOOK_SECRET=
|
|
774
733
|
RC_ENTITLEMENT_ID=pro
|
|
775
734
|
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
|
-
: '';
|
|
735
|
+
`;
|
|
785
736
|
writeFile(path.join(root, '.env.example'), `# Apps
|
|
786
737
|
FRONTEND_URL=${frontendUrl}
|
|
787
738
|
ANHEDRAL_DEMO=false
|
|
788
|
-
${
|
|
739
|
+
${expoEnv}${extensionEnv}
|
|
789
740
|
|
|
790
741
|
# Server
|
|
791
742
|
PORT=8787
|
|
@@ -801,126 +752,123 @@ R2_ACCESS_KEY_ID=
|
|
|
801
752
|
R2_SECRET_ACCESS_KEY=
|
|
802
753
|
R2_BUCKET=
|
|
803
754
|
|
|
804
|
-
${
|
|
755
|
+
${paymentsEnv}`);
|
|
805
756
|
}
|
|
806
|
-
function
|
|
807
|
-
const builds = options.frontend === 'next'
|
|
808
|
-
? [
|
|
809
|
-
{ src: 'apps/web/package.json', use: '@vercel/next' },
|
|
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');
|
|
757
|
+
function writeRootVercelFiles(root) {
|
|
825
758
|
writeFile(path.join(root, '.vercelignore'), `apps/extension/.output
|
|
826
759
|
apps/extension/.wxt
|
|
827
760
|
apps/extension/dist
|
|
828
|
-
apps/mobile/.expo
|
|
829
761
|
`);
|
|
830
762
|
}
|
|
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
|
-
|
|
763
|
+
function writeGeneratedCiWorkflow(root) {
|
|
764
|
+
writeFile(path.join(root, '.github/workflows/ci.yml'), `name: CI
|
|
765
|
+
|
|
766
|
+
on:
|
|
767
|
+
pull_request:
|
|
768
|
+
push:
|
|
769
|
+
branches:
|
|
770
|
+
- main
|
|
771
|
+
|
|
772
|
+
permissions:
|
|
773
|
+
contents: read
|
|
774
|
+
|
|
775
|
+
concurrency:
|
|
776
|
+
group: ci-\${{ github.workflow }}-\${{ github.ref }}
|
|
777
|
+
cancel-in-progress: true
|
|
778
|
+
|
|
779
|
+
jobs:
|
|
780
|
+
verify:
|
|
781
|
+
runs-on: ubuntu-latest
|
|
782
|
+
timeout-minutes: 45
|
|
783
|
+
steps:
|
|
784
|
+
- name: Checkout
|
|
785
|
+
uses: actions/checkout@v4
|
|
786
|
+
|
|
787
|
+
- name: Setup pnpm
|
|
788
|
+
uses: pnpm/action-setup@v4
|
|
789
|
+
with:
|
|
790
|
+
version: 10.15.1
|
|
791
|
+
|
|
792
|
+
- name: Setup Node.js
|
|
793
|
+
uses: actions/setup-node@v4
|
|
794
|
+
with:
|
|
795
|
+
node-version: 20
|
|
796
|
+
cache: pnpm
|
|
797
|
+
|
|
798
|
+
- name: Install dependencies
|
|
799
|
+
run: pnpm install --frozen-lockfile
|
|
800
|
+
|
|
801
|
+
- name: Check Expo dependency alignment
|
|
802
|
+
run: pnpm --filter ./apps/frontend exec expo install --check
|
|
803
|
+
|
|
804
|
+
- name: Build Expo web
|
|
805
|
+
run: pnpm --filter ./apps/frontend build:web
|
|
806
|
+
|
|
807
|
+
- name: Test API
|
|
808
|
+
run: pnpm --filter ./apps/api test
|
|
809
|
+
|
|
810
|
+
- name: Build API
|
|
811
|
+
run: pnpm --filter ./apps/api build
|
|
812
|
+
|
|
813
|
+
- name: Build extension
|
|
814
|
+
run: pnpm --filter ./apps/extension build
|
|
815
|
+
|
|
816
|
+
- name: Zip extension
|
|
817
|
+
run: pnpm --filter ./apps/extension zip
|
|
818
|
+
|
|
819
|
+
- name: Build workspace
|
|
820
|
+
run: pnpm build
|
|
821
|
+
`);
|
|
886
822
|
}
|
|
887
823
|
function writeFullstackRootFiles(root, options) {
|
|
888
824
|
const appFilters = [
|
|
889
|
-
|
|
825
|
+
'./apps/frontend',
|
|
890
826
|
'./apps/api',
|
|
891
|
-
|
|
827
|
+
'./apps/extension',
|
|
892
828
|
];
|
|
893
|
-
const parallelFilters = appFilters.map((entry) => `--filter
|
|
894
|
-
const buildFilters = ['./packages/db', ...appFilters].map((entry) => `--filter ${entry}`).join(' ');
|
|
829
|
+
const parallelFilters = appFilters.map((entry) => `--filter=${entry}`).join(' ');
|
|
895
830
|
const scripts = {
|
|
896
|
-
dev: `
|
|
831
|
+
dev: `turbo dev --parallel ${parallelFilters}`,
|
|
897
832
|
'dev:api': 'pnpm --filter ./apps/api dev',
|
|
898
|
-
build:
|
|
899
|
-
typecheck: '
|
|
833
|
+
build: 'turbo build',
|
|
834
|
+
typecheck: 'turbo typecheck',
|
|
900
835
|
'db:generate': 'pnpm --filter @anhedral/db db:generate',
|
|
901
836
|
'db:migrate': 'pnpm --filter @anhedral/db db:migrate',
|
|
902
837
|
};
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
if (options.extension) {
|
|
910
|
-
scripts['dev:extension'] = 'pnpm --filter ./apps/extension dev';
|
|
911
|
-
scripts['extension:zip'] = 'pnpm --filter ./apps/extension zip';
|
|
912
|
-
}
|
|
913
|
-
writeRootPackageJson(root, options.projectName, scripts, ['apps/*', 'packages/*']);
|
|
838
|
+
scripts['dev:frontend'] = 'pnpm --filter ./apps/frontend dev';
|
|
839
|
+
scripts['dev:extension'] = 'pnpm --filter ./apps/extension dev';
|
|
840
|
+
scripts['extension:zip'] = 'pnpm --filter ./apps/extension zip';
|
|
841
|
+
writeRootPackageJson(root, options.projectName, scripts, ['apps/*', 'packages/*'], {
|
|
842
|
+
devDependencies: ROOT_DEPENDENCIES.devDependencies,
|
|
843
|
+
});
|
|
914
844
|
writePnpmWorkspace(root, ['apps/*', 'packages/*']);
|
|
845
|
+
writeFile(path.join(root, 'turbo.json'), JSON.stringify({
|
|
846
|
+
$schema: 'https://turborepo.dev/schema.json',
|
|
847
|
+
tasks: {
|
|
848
|
+
build: {
|
|
849
|
+
dependsOn: ['^build'],
|
|
850
|
+
outputs: ['dist/**', '.output/**'],
|
|
851
|
+
},
|
|
852
|
+
typecheck: {
|
|
853
|
+
dependsOn: ['^build'],
|
|
854
|
+
},
|
|
855
|
+
dev: {
|
|
856
|
+
cache: false,
|
|
857
|
+
persistent: true,
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
}, null, 2) + '\n');
|
|
915
861
|
appendGitignore(root, [
|
|
916
862
|
'node_modules',
|
|
863
|
+
'.turbo',
|
|
917
864
|
'apps/*/node_modules',
|
|
918
865
|
'packages/*/node_modules',
|
|
919
866
|
...SHARED_ENV_GITIGNORE_LINES,
|
|
920
867
|
...SHARED_TYPESCRIPT_GITIGNORE_LINES,
|
|
921
868
|
]);
|
|
922
|
-
writeRootEnvExample(root
|
|
923
|
-
|
|
869
|
+
writeRootEnvExample(root);
|
|
870
|
+
writeRootVercelFiles(root);
|
|
871
|
+
writeGeneratedCiWorkflow(root);
|
|
924
872
|
}
|
|
925
873
|
async function scaffoldFullstack(root, options) {
|
|
926
874
|
anhedralPrint.section('Workspace root');
|
|
@@ -931,36 +879,33 @@ async function scaffoldFullstack(root, options) {
|
|
|
931
879
|
anhedralPrint.step('Writing @anhedral/db, types, config, api-client');
|
|
932
880
|
writeSharedPackages(root);
|
|
933
881
|
anhedralPrint.done('Shared packages written');
|
|
934
|
-
const frontendUrl =
|
|
882
|
+
const frontendUrl = 'http://localhost:8081';
|
|
935
883
|
await scaffoldBackend(root, {
|
|
936
884
|
projectName: options.projectName,
|
|
937
885
|
displayName: options.displayName,
|
|
938
886
|
githubOrg: null,
|
|
939
887
|
frontendUrl,
|
|
888
|
+
skipInstall: options.skipInstall,
|
|
889
|
+
});
|
|
890
|
+
await scaffoldFrontend(root, {
|
|
891
|
+
projectName: options.projectName,
|
|
892
|
+
displayName: options.displayName,
|
|
893
|
+
githubOrg: null,
|
|
894
|
+
frontendUrl,
|
|
895
|
+
skipInstall: options.skipInstall,
|
|
896
|
+
});
|
|
897
|
+
await scaffoldExtension(root, {
|
|
898
|
+
projectName: options.projectName,
|
|
899
|
+
displayName: options.displayName,
|
|
900
|
+
githubOrg: null,
|
|
901
|
+
frontendUrl,
|
|
902
|
+
skipInstall: options.skipInstall,
|
|
940
903
|
});
|
|
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
904
|
return [
|
|
961
|
-
|
|
905
|
+
'.github/workflows/ci.yml',
|
|
906
|
+
'apps/frontend',
|
|
962
907
|
'apps/api',
|
|
963
|
-
|
|
908
|
+
'apps/extension',
|
|
964
909
|
'packages/db',
|
|
965
910
|
'packages/types',
|
|
966
911
|
'packages/config',
|
|
@@ -978,7 +923,7 @@ export async function scaffoldProject(options) {
|
|
|
978
923
|
const generatedPaths = await scaffoldFullstack(root, options);
|
|
979
924
|
anhedralPrint.section('Project metadata');
|
|
980
925
|
anhedralPrint.step('Writing skills guide, README, and stack.json');
|
|
981
|
-
const skillCommands = getSkillCommands(
|
|
926
|
+
const skillCommands = getSkillCommands();
|
|
982
927
|
writeSkillsGuide(root, skillCommands);
|
|
983
928
|
const stack = normalizeStack(options, generatedPaths);
|
|
984
929
|
writeRootDocs(root, options, stack);
|
|
@@ -993,18 +938,11 @@ export async function scaffoldProject(options) {
|
|
|
993
938
|
anhedralPrint.info('Next commands:');
|
|
994
939
|
console.log(' pnpm install');
|
|
995
940
|
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
|
-
}
|
|
941
|
+
console.log(' cp apps/extension/.env.example apps/extension/.env');
|
|
1002
942
|
console.log(' pnpm db:generate && pnpm db:migrate');
|
|
1003
943
|
console.log(' pnpm dev:api');
|
|
1004
|
-
console.log(
|
|
1005
|
-
|
|
1006
|
-
console.log(' pnpm dev:extension');
|
|
1007
|
-
}
|
|
944
|
+
console.log(' pnpm dev:frontend');
|
|
945
|
+
console.log(' pnpm dev:extension');
|
|
1008
946
|
console.log('');
|
|
1009
947
|
}
|
|
1010
948
|
finally {
|