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
|
@@ -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,
|
|
2
|
+
import { writeFile, execCommand } from '../util.js';
|
|
3
3
|
import { anhedralPrint } from '../print.js';
|
|
4
4
|
import { getBackendInstallCommands } from '../commands.js';
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
27
|
+
writeVercelConfig(dir);
|
|
27
28
|
writeTestFiles(dir);
|
|
28
29
|
anhedralPrint.done('Backend source files written');
|
|
29
|
-
if (
|
|
30
|
-
anhedralPrint.info('Skipping backend dependency install (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2159
|
-
writeFile(path.join(dir, '
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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'), `
|
|
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
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
450
|
-
return this.
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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>;
|