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.
@@ -1,2 +1,2 @@
1
1
  import type { ProjectOptions } from '../scaffold.js';
2
- export declare function scaffoldBackend(root: string, { projectName, displayName, frontendUrl }: ProjectOptions): Promise<void>;
2
+ export declare function scaffoldBackend(root: string, { projectName, displayName, frontendUrl, skipInstall }: ProjectOptions): Promise<void>;
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
- import { writeFile, exec } from '../util.js';
2
+ import { writeFile, execCommand } from '../util.js';
3
3
  import { anhedralPrint } from '../print.js';
4
4
  import { getBackendInstallCommands } from '../commands.js';
5
- export async function scaffoldBackend(root, { projectName, displayName, frontendUrl }) {
6
- const dir = path.join(root, 'apps/api');
5
+ import { BACKEND_DEPENDENCIES } from '../dependencies.js';
6
+ export async function scaffoldBackend(root, { projectName, displayName, frontendUrl, skipInstall }) {
7
+ const dir = path.join(root, 'Backend');
7
8
  anhedralPrint.section('Backend (Fastify)');
8
9
  anhedralPrint.step('Writing backend source files');
9
10
  writePackageJson(dir, projectName);
@@ -23,16 +24,17 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
23
24
  writeRepositories(dir);
24
25
  writeRoutes(dir, displayName);
25
26
  writeAppAndIndex(dir, displayName);
26
- writeVercelEntry(dir);
27
+ writeVercelConfig(dir);
27
28
  writeTestFiles(dir);
28
29
  anhedralPrint.done('Backend source files written');
29
- if (process.env.ANHEDRAL_SKIP_INSTALL === '1') {
30
- anhedralPrint.info('Skipping backend dependency install (ANHEDRAL_SKIP_INSTALL=1)');
30
+ if (skipInstall) {
31
+ anhedralPrint.info('Skipping backend dependency install (--skip-install)');
32
+ anhedralPrint.info('Run after init: pnpm install');
31
33
  return;
32
34
  }
33
35
  anhedralPrint.step('Installing backend dependencies');
34
36
  for (const command of getBackendInstallCommands()) {
35
- exec(command.cmd, dir);
37
+ execCommand(command.command, command.args, dir);
36
38
  }
37
39
  anhedralPrint.done('Backend dependencies installed');
38
40
  }
@@ -59,9 +61,8 @@ function writePackageJson(dir, projectName) {
59
61
  },
60
62
  keywords: [],
61
63
  license: 'MIT',
62
- dependencies: {
63
- '@anhedral/db': 'workspace:*',
64
- },
64
+ dependencies: BACKEND_DEPENDENCIES.dependencies,
65
+ devDependencies: BACKEND_DEPENDENCIES.devDependencies,
65
66
  }, null, 2) + '\n');
66
67
  }
67
68
  function writeTsConfig(dir) {
@@ -1143,8 +1144,8 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
1143
1144
  R2_ACCESS_KEY_ID: { type: 'string', nullable: true },
1144
1145
  R2_SECRET_ACCESS_KEY: { type: 'string', nullable: true },
1145
1146
  R2_BUCKET: { type: 'string', nullable: true },
1146
- RC_SECRET_API_KEY: { type: 'string', default: '' },
1147
- RC_WEBHOOK_SECRET: { type: 'string', default: '' },
1147
+ RC_SECRET_API_KEY: { type: 'string', nullable: true, default: '' },
1148
+ RC_WEBHOOK_SECRET: { type: 'string', nullable: true, default: '' },
1148
1149
  RC_ENTITLEMENT_ID: { type: 'string', default: 'pro' },
1149
1150
  RC_OFFERING_ID: { type: 'string', default: 'default' },
1150
1151
  },
@@ -1155,6 +1156,23 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
1155
1156
  dotenv: !process.env.VERCEL,
1156
1157
  confKey: 'env',
1157
1158
  } as unknown as Record<string, unknown>);
1159
+
1160
+ if (fastify.env.NODE_ENV === 'production') {
1161
+ if (fastify.env.ANHEDRAL_DEMO === 'true') {
1162
+ throw new Error('ANHEDRAL_DEMO must be false in production');
1163
+ }
1164
+
1165
+ const required = [
1166
+ 'CLERK_PUBLISHABLE_KEY',
1167
+ 'CLERK_SECRET_KEY',
1168
+ 'RC_SECRET_API_KEY',
1169
+ 'RC_WEBHOOK_SECRET',
1170
+ ] as const;
1171
+ const missing = required.filter((key) => !fastify.env[key]);
1172
+ if (missing.length > 0) {
1173
+ throw new Error(\`Missing production environment variables: \${missing.join(', ')}\`);
1174
+ }
1175
+ }
1158
1176
  };
1159
1177
 
1160
1178
  export default fp(configPlugin, { name: 'env-config', fastify: '5.x' });
@@ -2155,39 +2173,12 @@ main().catch((err) => {
2155
2173
  });
2156
2174
  `);
2157
2175
  }
2158
- function writeVercelEntry(dir) {
2159
- writeFile(path.join(dir, 'api/index.ts'), `import type { VercelRequest, VercelResponse } from '@vercel/node';
2160
- import type { FastifyInstance } from 'fastify';
2161
- import { buildApp } from '../src/app.js';
2162
-
2163
- let appReadyPromise: Promise<FastifyInstance> | undefined;
2164
-
2165
- async function getApp(): Promise<FastifyInstance> {
2166
- if (!appReadyPromise) {
2167
- appReadyPromise = (async () => {
2168
- const app = await buildApp();
2169
- await app.ready();
2170
- return app;
2171
- })().catch((err) => {
2172
- appReadyPromise = undefined;
2173
- throw err;
2174
- });
2175
- }
2176
- return appReadyPromise;
2177
- }
2178
-
2179
- export default async function handler(req: VercelRequest, res: VercelResponse) {
2180
- const app = await getApp();
2181
- if (req.url === '/api' || req.url === '/backend') {
2182
- req.url = '/';
2183
- } else if (req.url?.startsWith('/api/')) {
2184
- req.url = req.url.slice('/api'.length);
2185
- } else if (req.url?.startsWith('/backend/')) {
2186
- req.url = req.url.slice('/backend'.length);
2187
- }
2188
- app.server.emit('request', req, res);
2189
- }
2190
- `);
2176
+ function writeVercelConfig(dir) {
2177
+ writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
2178
+ $schema: 'https://openapi.vercel.sh/vercel.json',
2179
+ buildCommand: 'pnpm build',
2180
+ devCommand: 'vercel dev',
2181
+ }, null, 2) + '\n');
2191
2182
  }
2192
2183
  // ═══════════════════════════════════════════════════════════════════════════
2193
2184
  // Tests
@@ -1,2 +1,2 @@
1
1
  import type { ProjectOptions } from '../scaffold.js';
2
- export declare function scaffoldExtension(root: string, { projectName, displayName }: ProjectOptions): Promise<void>;
2
+ export declare function scaffoldExtension(root: string, { projectName, displayName, skipInstall }: ProjectOptions): Promise<void>;
@@ -2,21 +2,25 @@ import { rmSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { writeFile, exec } from '../util.js';
4
4
  import { anhedralPrint } from '../print.js';
5
+ import { EXTENSION_DEPENDENCIES } from '../dependencies.js';
5
6
  import { resolveToolchainChannel, resolveToolchain, toolPackageRef } from '../toolchain.js';
6
- export async function scaffoldExtension(root, { projectName, displayName }) {
7
- const appsRoot = path.join(root, 'apps');
8
- const dir = path.join(appsRoot, 'extension');
7
+ export async function scaffoldExtension(root, { projectName, displayName, skipInstall }) {
8
+ const dir = path.join(root, 'Extension');
9
9
  const toolchain = resolveToolchain(resolveToolchainChannel(process.env.ANHEDRAL_TOOLCHAIN));
10
10
  anhedralPrint.section('Chrome extension (WXT)');
11
11
  anhedralPrint.step('Scaffolding WXT extension');
12
- writeFile(path.join(appsRoot, '.gitkeep'), '');
13
- exec(`pnpm dlx ${toolPackageRef('wxt', toolchain.wxt)} init extension -t react --pm pnpm`, appsRoot);
12
+ exec(`pnpm dlx --allow-build=esbuild --allow-build=spawn-sync ${toolPackageRef('wxt', toolchain.wxt)} init Extension -t react --pm pnpm`, root);
14
13
  writePackageJson(dir, projectName);
15
14
  anhedralPrint.done('WXT extension scaffolded');
16
15
  anhedralPrint.step('Installing Clerk + React + Tailwind dependencies');
17
- exec(`pnpm add -D @types/chrome @types/react @types/react-dom @wxt-dev/module-react autoprefixer postcss tailwindcss@3.4.19 typescript@5.9.3 ${toolPackageRef('wxt', toolchain.wxt)}`, dir);
18
- exec('pnpm add @clerk/chrome-extension react react-dom clsx tailwind-merge class-variance-authority lucide-react@0.468.0', dir);
19
- anhedralPrint.done('Extension dependencies installed');
16
+ if (skipInstall) {
17
+ anhedralPrint.info('Skipping extension dependency install (--skip-install)');
18
+ anhedralPrint.info('Run after init: pnpm install');
19
+ }
20
+ else {
21
+ exec('pnpm install --no-frozen-lockfile', dir);
22
+ }
23
+ anhedralPrint.done(skipInstall ? 'Extension dependency manifests written' : 'Extension dependencies installed');
20
24
  cleanWxtStarterFiles(dir);
21
25
  writeWxtConfig(dir, displayName);
22
26
  writeTsConfig(dir);
@@ -27,6 +31,7 @@ export async function scaffoldExtension(root, { projectName, displayName }) {
27
31
  writeShadcnConfig(dir);
28
32
  writeReadme(dir, displayName);
29
33
  writeCnUtil(dir);
34
+ writeButtonComponent(dir);
30
35
  writeAuthContext(dir);
31
36
  writeApiClient(dir);
32
37
  writeBackground(dir);
@@ -35,10 +40,7 @@ export async function scaffoldExtension(root, { projectName, displayName }) {
35
40
  writeSidepanelHtml(dir, displayName);
36
41
  writeSidepanelApp(dir);
37
42
  writeStyles(dir);
38
- anhedralPrint.step('Adding shadcn button component');
39
- exec(`pnpm dlx ${toolPackageRef('shadcn', toolchain.shadcn)} add button`, dir);
40
- writeStyles(dir);
41
- anhedralPrint.done('shadcn button installed');
43
+ anhedralPrint.done('Extension source files written');
42
44
  }
43
45
  function cleanWxtStarterFiles(dir) {
44
46
  for (const relativePath of [
@@ -65,6 +67,8 @@ function writePackageJson(dir, projectName) {
65
67
  'zip:firefox': 'wxt zip -b firefox',
66
68
  typecheck: 'tsc --noEmit',
67
69
  },
70
+ dependencies: EXTENSION_DEPENDENCIES.dependencies,
71
+ devDependencies: EXTENSION_DEPENDENCIES.devDependencies,
68
72
  }, null, 2) + '\n');
69
73
  }
70
74
  function writeWxtConfig(dir, displayName) {
@@ -80,14 +84,23 @@ export default defineConfig({
80
84
  description: '${displayName} Chrome Extension',
81
85
  version: '0.1.0',
82
86
  ...(crxPublicKey ? { key: crxPublicKey } : {}),
83
- permissions: ['activeTab', 'cookies', 'storage'],
87
+ minimum_chrome_version: '114',
88
+ permissions: ['activeTab', 'cookies', 'storage', 'sidePanel'],
84
89
  host_permissions: [],
85
90
  action: {
86
91
  default_title: 'Open ${displayName}',
87
92
  },
93
+ side_panel: {
94
+ default_path: 'sidepanel.html',
95
+ },
88
96
  };
89
97
  },
90
98
  modules: ['@wxt-dev/module-react'],
99
+ vite: () => ({
100
+ build: {
101
+ chunkSizeWarningLimit: 3000,
102
+ },
103
+ }),
91
104
  });
92
105
  `);
93
106
  }
@@ -108,7 +121,6 @@ function writeTsConfig(dir) {
108
121
  jsx: 'react-jsx',
109
122
  lib: ['DOM', 'DOM.Iterable', 'ESNext'],
110
123
  types: ['wxt/browser', '@wxt-dev/module-react', 'chrome'],
111
- baseUrl: '.',
112
124
  paths: { '@/*': ['./src/*'] },
113
125
  },
114
126
  include: ['**/*.ts', '**/*.tsx', '.wxt/wxt.d.ts'],
@@ -190,6 +202,41 @@ export function cn(...inputs: ClassValue[]) {
190
202
  }
191
203
  `);
192
204
  }
205
+ function writeButtonComponent(dir) {
206
+ writeFile(path.join(dir, 'src/components/ui/button.tsx'), `import * as React from 'react';
207
+ import { cn } from '@/lib/utils';
208
+
209
+ type ButtonVariant = 'default' | 'outline';
210
+
211
+ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
212
+ variant?: ButtonVariant;
213
+ };
214
+
215
+ const variantClasses: Record<ButtonVariant, string> = {
216
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
217
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
218
+ };
219
+
220
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
221
+ ({ className, variant = 'default', type = 'button', ...props }, ref) => {
222
+ return (
223
+ <button
224
+ ref={ref}
225
+ type={type}
226
+ className={cn(
227
+ 'inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50',
228
+ variantClasses[variant],
229
+ className,
230
+ )}
231
+ {...props}
232
+ />
233
+ );
234
+ },
235
+ );
236
+
237
+ Button.displayName = 'Button';
238
+ `);
239
+ }
193
240
  function writeTailwindConfig(dir) {
194
241
  writeFile(path.join(dir, 'tailwind.config.cjs'), `/** @type {import('tailwindcss').Config} */
195
242
  module.exports = {
@@ -261,7 +308,7 @@ Set \`VITE_CLERK_PUBLISHABLE_KEY\` and \`VITE_API_URL\` in \`.env\` before using
261
308
 
262
309
  Run \`pnpm build\`, then load \`.output/chrome-mv3\` as an unpacked extension from \`chrome://extensions\`.
263
310
 
264
- The browser action opens the side panel. The background script initializes Clerk, the side panel handles sign-in/subscription state, and the content script is ready for page-to-extension messages.
311
+ The extension uses Chrome's Side Panel API. The browser action opens \`sidepanel.html\`, \`wxt.config.ts\` declares the \`sidePanel\` permission and Chrome 114+ minimum version, the background script initializes Clerk, and the content script is ready for page-to-extension messages.
265
312
  `);
266
313
  }
267
314
  function writeAuthContext(dir) {
@@ -405,58 +452,25 @@ export { WEBSITE_URL };
405
452
  `);
406
453
  }
407
454
  function writeApiClient(dir) {
408
- writeFile(path.join(dir, 'src/lib/api.ts'), `const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8787';
409
-
410
- export type SubscriptionEntitlements = {
411
- pro: boolean;
412
- inTrial: boolean;
413
- trialEndsAt?: string;
414
- expiresAt?: string;
415
- periodStart?: string;
416
- periodEnd?: string;
417
- method?: 'trialing' | 'redeemed' | 'paid' | null;
418
- managementUrl?: string;
419
- cancelAtPeriodEnd?: boolean;
420
- };
455
+ writeFile(path.join(dir, 'src/lib/api.ts'), `import { ApiClient } from '@anhedral/api-client';
421
456
 
422
457
  export class APIClient {
423
458
  constructor(private getToken: () => Promise<string | null>) {}
424
459
 
425
- private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
426
- const token = await this.getToken();
427
- if (!token) throw new Error('Not authenticated');
428
-
429
- const headers: Record<string, string> = {
430
- Authorization: \`Bearer \${token}\`,
431
- 'Content-Type': 'application/json',
432
- 'X-Client-Type': 'chrome-extension',
433
- 'X-Platform': 'chrome',
434
- ...(options.headers as Record<string, string>),
435
- };
436
-
437
- const response = await fetch(\`\${API_BASE_URL}\${endpoint}\`, { ...options, headers });
438
-
439
- if (!response.ok) {
440
- let error: { message?: string } = {};
441
- try { error = await response.json(); } catch {}
442
- throw new Error(error.message || \`Request failed (\${response.status})\`);
443
- }
444
-
445
- if (response.status === 204) return {} as T;
446
- return response.json();
460
+ private client() {
461
+ return new ApiClient({
462
+ baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:8787',
463
+ getToken: this.getToken,
464
+ platform: 'extension',
465
+ });
447
466
  }
448
467
 
449
- async getMe() {
450
- return this.request<{
451
- user: { id: string; email: string; subscriptionTier: string; subscriptionStatus: string };
452
- }>('/auth/me');
468
+ getMe() {
469
+ return this.client().getMe();
453
470
  }
454
471
 
455
- async getSubscriptionEntitlements(options?: { refresh?: boolean }): Promise<SubscriptionEntitlements> {
456
- const path = options?.refresh
457
- ? '/subscriptions/entitlements/me?refresh=true'
458
- : '/subscriptions/entitlements/me';
459
- return this.request<SubscriptionEntitlements>(path);
472
+ getSubscriptionEntitlements(options?: { refresh?: boolean }) {
473
+ return this.client().getSubscriptionEntitlements(options);
460
474
  }
461
475
  }
462
476
  `);
@@ -464,15 +478,20 @@ export class APIClient {
464
478
  function writeBackground(dir) {
465
479
  writeFile(path.join(dir, 'src/entrypoints/background.ts'), `import { createClerkClient } from '@clerk/chrome-extension/background';
466
480
 
481
+ type ChromeWithSidePanel = typeof chrome & {
482
+ sidePanel: {
483
+ setPanelBehavior: (behavior: { openPanelOnActionClick: boolean }) => Promise<void>;
484
+ };
485
+ };
486
+
467
487
  export default defineBackground(() => {
468
488
  // Initialize Clerk in the background script for cookie-based auth
469
489
  void createClerkClient({
470
490
  publishableKey: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || '',
471
491
  }).catch(() => {});
472
492
 
473
- // Open the side panel when the extension icon is clicked
474
- // The sidePanel permission is auto-added by WXT when the sidepanel entrypoint exists
475
- chrome.sidePanel
493
+ // Open the side panel when the extension icon is clicked.
494
+ (chrome as ChromeWithSidePanel).sidePanel
476
495
  .setPanelBehavior({ openPanelOnActionClick: true })
477
496
  .catch(() => {});
478
497
  });
@@ -1,2 +1,2 @@
1
1
  import type { ProjectOptions } from '../scaffold.js';
2
- export declare function scaffoldFrontend(root: string, { projectName, displayName }: ProjectOptions): Promise<void>;
2
+ export declare function scaffoldFrontend(root: string, { projectName, displayName, skipInstall }: ProjectOptions): Promise<void>;