anhedral 0.1.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/CONTRIBUTING.md +70 -0
  3. package/LICENSE +21 -17
  4. package/README.md +151 -152
  5. package/SECURITY.md +35 -0
  6. package/SKILL.md +177 -0
  7. package/bin/anhedral.js +1 -1
  8. package/dist/architecture/contributions.d.ts +28 -0
  9. package/dist/architecture/contributions.js +118 -0
  10. package/dist/architecture/index.d.ts +12 -0
  11. package/dist/architecture/index.js +6 -0
  12. package/dist/architecture/manifest.d.ts +53 -0
  13. package/dist/architecture/manifest.js +283 -0
  14. package/dist/architecture/modules.d.ts +37 -0
  15. package/dist/architecture/modules.js +176 -0
  16. package/dist/architecture/plan.d.ts +58 -0
  17. package/dist/architecture/plan.js +145 -0
  18. package/dist/architecture/templates.d.ts +10 -0
  19. package/dist/architecture/templates.js +23 -0
  20. package/dist/bin.d.ts +2 -0
  21. package/dist/bin.js +167 -0
  22. package/dist/cli.d.ts +24 -2
  23. package/dist/cli.js +186 -21
  24. package/dist/composers/root.d.ts +6 -0
  25. package/dist/composers/root.js +34 -0
  26. package/dist/dependencies.d.ts +29 -9
  27. package/dist/dependencies.js +220 -60
  28. package/dist/index.d.ts +8 -2
  29. package/dist/index.js +5 -31
  30. package/dist/print.js +15 -0
  31. package/dist/prompts.d.ts +5 -0
  32. package/dist/prompts.js +19 -0
  33. package/dist/render.d.ts +11 -0
  34. package/dist/render.js +74 -0
  35. package/dist/scaffold.d.ts +56 -12
  36. package/dist/scaffold.js +1466 -1158
  37. package/dist/template-source.d.ts +20 -0
  38. package/dist/template-source.js +180 -0
  39. package/dist/templates/api.d.ts +2 -0
  40. package/dist/templates/api.js +3114 -0
  41. package/dist/templates/assets-private-proxy.d.ts +3 -0
  42. package/dist/templates/assets-private-proxy.js +277 -0
  43. package/dist/templates/desktop.d.ts +2 -0
  44. package/dist/templates/desktop.js +572 -0
  45. package/dist/templates/extension.d.ts +1 -1
  46. package/dist/templates/extension.js +322 -248
  47. package/dist/templates/mobile.d.ts +2 -0
  48. package/dist/templates/mobile.js +720 -0
  49. package/dist/templates/shared.d.ts +2 -0
  50. package/dist/templates/shared.js +503 -0
  51. package/dist/templates/web.d.ts +2 -0
  52. package/dist/templates/web.js +412 -0
  53. package/dist/toolchain.d.ts +0 -8
  54. package/dist/toolchain.js +0 -21
  55. package/dist/transaction.d.ts +22 -0
  56. package/dist/transaction.js +822 -0
  57. package/dist/ui.d.ts +33 -0
  58. package/dist/ui.js +199 -0
  59. package/dist/util.d.ts +1 -5
  60. package/dist/util.js +70 -159
  61. package/dist/version.d.ts +2 -0
  62. package/dist/version.js +2 -0
  63. package/dist/workspace-config.d.ts +11 -0
  64. package/dist/workspace-config.js +372 -0
  65. package/docs/conventions.md +89 -0
  66. package/docs/output-tree-contract.md +442 -0
  67. package/package.json +56 -13
  68. package/references/manual-scaffolding.md +386 -0
  69. package/templates/api-fastify/apps/api/tsconfig.json +20 -0
  70. package/templates/catalog.json +23 -0
  71. package/templates/db-drizzle/packages/db/tsconfig.json +14 -0
  72. package/templates/desktop-electron/apps/desktop/tsconfig.json +37 -0
  73. package/templates/extension-wxt/apps/extension/tsconfig.json +41 -0
  74. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +1 -0
  75. package/templates/web-next/apps/web/next-env.d.ts +6 -0
  76. package/dist/branding.d.ts +0 -5
  77. package/dist/branding.js +0 -17
  78. package/dist/commands.d.ts +0 -7
  79. package/dist/commands.js +0 -24
  80. package/dist/templates/backend.d.ts +0 -2
  81. package/dist/templates/backend.js +0 -2115
  82. package/dist/templates/frontend.d.ts +0 -2
  83. package/dist/templates/frontend.js +0 -1268
@@ -0,0 +1,572 @@
1
+ import path from 'node:path';
2
+ import { writeFile } from '../util.js';
3
+ import { anhedralPrint } from '../print.js';
4
+ import { DESKTOP_DEPENDENCIES } from '../dependencies.js';
5
+ import { childPackageName, htmlText, identifierSegment, jsString } from '../render.js';
6
+ function selectedDependencies(options) {
7
+ const dependencies = { ...(DESKTOP_DEPENDENCIES.dependencies ?? {}) };
8
+ if (!options.apps.api)
9
+ delete dependencies['@shared/api-client'];
10
+ if (!options.features.billing)
11
+ delete dependencies['@shared/realtime'];
12
+ if (!options.features.auth) {
13
+ delete dependencies['@clerk/clerk-js'];
14
+ delete dependencies['@clerk/ui'];
15
+ delete dependencies['@solana/web3.js'];
16
+ }
17
+ return dependencies;
18
+ }
19
+ export async function scaffoldDesktop(root, options) {
20
+ const { projectName, displayName } = options;
21
+ const dir = path.join(root, 'apps/desktop');
22
+ anhedralPrint.section('Desktop (Electron + shadcn/ui)');
23
+ anhedralPrint.step('Writing Electron desktop app');
24
+ writePackageJson(dir, projectName, options);
25
+ writeTsConfig(dir);
26
+ writeViteConfig(dir);
27
+ writePostcssConfig(dir);
28
+ writeDevScript(dir);
29
+ writeShadcnConfig(dir);
30
+ writeEnvExample(dir, options);
31
+ writeSourceFiles(dir, displayName, options);
32
+ anhedralPrint.done('Electron desktop app written');
33
+ }
34
+ function writePackageJson(dir, projectName, options) {
35
+ writeFile(path.join(dir, 'package.json'), JSON.stringify({
36
+ name: childPackageName(projectName, 'desktop'),
37
+ version: '0.1.0',
38
+ private: true,
39
+ type: 'module',
40
+ main: 'dist/main/main.js',
41
+ scripts: {
42
+ dev: 'tsc -p tsconfig.main.json && node scripts/dev.mjs',
43
+ build: 'tsc --noEmit && tsc -p tsconfig.main.json && vite build',
44
+ typecheck: 'tsc --noEmit',
45
+ 'build:mac': 'pnpm build && electron-builder --mac',
46
+ 'build:win': 'pnpm build && electron-builder --win',
47
+ 'build:linux': 'pnpm build && electron-builder --linux',
48
+ package: 'pnpm build && electron-builder',
49
+ },
50
+ build: {
51
+ appId: `dev.anhedral.${identifierSegment(projectName)}`,
52
+ productName: projectName,
53
+ directories: {
54
+ output: 'release',
55
+ },
56
+ files: [
57
+ 'dist/**/*',
58
+ 'package.json',
59
+ ],
60
+ mac: {
61
+ target: ['dmg', 'zip'],
62
+ },
63
+ win: {
64
+ target: ['nsis', 'zip'],
65
+ },
66
+ linux: {
67
+ executableName: identifierSegment(projectName),
68
+ target: ['AppImage', 'deb'],
69
+ },
70
+ },
71
+ dependencies: selectedDependencies(options),
72
+ devDependencies: DESKTOP_DEPENDENCIES.devDependencies,
73
+ }, null, 2) + '\n');
74
+ }
75
+ function writeTsConfig(dir) {
76
+ writeFile(path.join(dir, 'tsconfig.main.json'), JSON.stringify({
77
+ compilerOptions: {
78
+ target: 'ES2022',
79
+ module: 'NodeNext',
80
+ moduleResolution: 'NodeNext',
81
+ strict: true,
82
+ skipLibCheck: true,
83
+ esModuleInterop: true,
84
+ outDir: 'dist/main',
85
+ rootDir: 'src/main',
86
+ types: ['node'],
87
+ },
88
+ include: ['src/main/**/*'],
89
+ }, null, 2) + '\n');
90
+ }
91
+ function writeViteConfig(dir) {
92
+ writeFile(path.join(dir, 'vite.config.ts'), `import { defineConfig } from 'vite';
93
+ import react from '@vitejs/plugin-react';
94
+ import path from 'node:path';
95
+
96
+ export default defineConfig({
97
+ base: './',
98
+ plugins: [react()],
99
+ resolve: {
100
+ alias: {
101
+ '@': path.resolve(__dirname, 'src/renderer'),
102
+ },
103
+ },
104
+ build: {
105
+ outDir: 'dist/renderer',
106
+ emptyOutDir: true,
107
+ },
108
+ });
109
+ `);
110
+ }
111
+ function writePostcssConfig(dir) {
112
+ writeFile(path.join(dir, 'postcss.config.mjs'), `const config = {
113
+ plugins: {
114
+ '@tailwindcss/postcss': {},
115
+ },
116
+ };
117
+
118
+ export default config;
119
+ `);
120
+ }
121
+ function writeDevScript(dir) {
122
+ writeFile(path.join(dir, 'scripts/dev.mjs'), `import { spawn } from 'node:child_process';
123
+ import { once } from 'node:events';
124
+
125
+ const host = '127.0.0.1';
126
+ const port = '5173';
127
+ const devServerUrl = 'http://' + host + ':' + port;
128
+ const viteCommand = process.platform === 'win32' ? 'vite.cmd' : 'vite';
129
+ const electronCommand = process.platform === 'win32' ? 'electron.cmd' : 'electron';
130
+
131
+ function start(command, args, options = {}) {
132
+ const invocation = process.platform === 'win32'
133
+ ? {
134
+ command: process.env.ComSpec || 'cmd.exe',
135
+ args: ['/d', '/s', '/c', 'call', command, ...args],
136
+ }
137
+ : { command, args };
138
+ return spawn(invocation.command, invocation.args, {
139
+ cwd: process.cwd(),
140
+ stdio: 'inherit',
141
+ ...options,
142
+ });
143
+ }
144
+
145
+ function stop(child) {
146
+ if (child && child.exitCode === null && !child.killed) child.kill('SIGTERM');
147
+ }
148
+
149
+ async function waitForServer(server, url, timeoutMs = 30_000) {
150
+ const deadline = Date.now() + timeoutMs;
151
+ while (Date.now() < deadline) {
152
+ if (server.exitCode !== null) throw new Error('Vite exited before the dev server was ready.');
153
+ try {
154
+ const response = await fetch(url);
155
+ if (response.ok) return;
156
+ } catch {
157
+ // The server is still starting.
158
+ }
159
+ await new Promise((resolve) => setTimeout(resolve, 150));
160
+ }
161
+ throw new Error('Timed out waiting for Vite at ' + url + '.');
162
+ }
163
+
164
+ const vite = start(viteCommand, ['--host', host, '--port', port, '--strictPort']);
165
+ let electron;
166
+ let shuttingDown = false;
167
+
168
+ for (const signal of ['SIGINT', 'SIGTERM']) {
169
+ process.once(signal, () => {
170
+ shuttingDown = true;
171
+ stop(electron);
172
+ stop(vite);
173
+ });
174
+ }
175
+
176
+ try {
177
+ await waitForServer(vite, devServerUrl);
178
+ electron = start(electronCommand, ['.'], {
179
+ env: { ...process.env, VITE_DEV_SERVER_URL: devServerUrl },
180
+ });
181
+ vite.once('exit', (code) => {
182
+ if (!shuttingDown) {
183
+ console.error('Vite exited while Electron was running.');
184
+ process.exitCode = code ?? 1;
185
+ stop(electron);
186
+ }
187
+ });
188
+ const [code] = await once(electron, 'exit');
189
+ if (process.exitCode === undefined) process.exitCode = typeof code === 'number' ? code : 1;
190
+ } catch (error) {
191
+ console.error(error instanceof Error ? error.message : error);
192
+ process.exitCode = 1;
193
+ } finally {
194
+ shuttingDown = true;
195
+ stop(electron);
196
+ stop(vite);
197
+ }
198
+ `);
199
+ }
200
+ function writeShadcnConfig(dir) {
201
+ writeFile(path.join(dir, 'components.json'), JSON.stringify({
202
+ $schema: 'https://ui.shadcn.com/schema.json',
203
+ style: 'new-york',
204
+ rsc: false,
205
+ tsx: true,
206
+ tailwind: {
207
+ config: '',
208
+ css: 'src/renderer/styles.css',
209
+ baseColor: 'neutral',
210
+ cssVariables: true,
211
+ prefix: '',
212
+ },
213
+ aliases: {
214
+ components: '@/components',
215
+ utils: '@/lib/utils',
216
+ ui: '@/components/ui',
217
+ lib: '@/lib',
218
+ hooks: '@/hooks',
219
+ },
220
+ iconLibrary: 'lucide',
221
+ }, null, 2) + '\n');
222
+ }
223
+ function writeEnvExample(dir, options) {
224
+ const lines = [
225
+ options.apps.api ? 'VITE_API_URL=http://localhost:8787/api' : null,
226
+ options.features.auth ? 'VITE_CLERK_PUBLISHABLE_KEY=pk_test_***' : null,
227
+ ].filter((value) => value !== null);
228
+ writeFile(path.join(dir, '.env.example'), lines.join('\n') + (lines.length > 0 ? '\n' : ''));
229
+ }
230
+ function writeSourceFiles(dir, displayName, options) {
231
+ const displayNameLiteral = jsString(displayName);
232
+ const displayNameHtml = htmlText(displayName);
233
+ const description = options.apps.api
234
+ ? 'Electron + shadcn/ui desktop client using the same shared API client as web, mobile, and extension.'
235
+ : 'Electron + shadcn/ui desktop client ready for your application.';
236
+ const buttonLabel = options.features.auth ? 'Open account' : options.apps.api ? 'Explore API' : 'Get started';
237
+ writeFile(path.join(dir, 'src/main/main.ts'), `import { app, BrowserWindow, session, shell } from 'electron';
238
+ import path from 'node:path';
239
+ import { fileURLToPath, pathToFileURL } from 'node:url';
240
+
241
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
242
+ const rendererIndex = path.join(__dirname, '../renderer/index.html');
243
+
244
+ function isSafeExternalUrl(value: string): boolean {
245
+ try {
246
+ const url = new URL(value);
247
+ return url.protocol === 'https:' && !url.username && !url.password;
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+
253
+ function isAllowedAppNavigation(value: string): boolean {
254
+ try {
255
+ const target = new URL(value);
256
+ if (process.env.VITE_DEV_SERVER_URL) {
257
+ return target.origin === new URL(process.env.VITE_DEV_SERVER_URL).origin;
258
+ }
259
+ return target.href === pathToFileURL(rendererIndex).href;
260
+ } catch {
261
+ return false;
262
+ }
263
+ }
264
+
265
+ function createWindow() {
266
+ const window = new BrowserWindow({
267
+ width: 1180,
268
+ height: 760,
269
+ minWidth: 860,
270
+ minHeight: 560,
271
+ title: ${displayNameLiteral},
272
+ webPreferences: {
273
+ preload: path.join(__dirname, 'preload.cjs'),
274
+ contextIsolation: true,
275
+ nodeIntegration: false,
276
+ sandbox: true,
277
+ webSecurity: true,
278
+ },
279
+ });
280
+
281
+ window.webContents.on('will-navigate', (event, url) => {
282
+ if (!isAllowedAppNavigation(url)) event.preventDefault();
283
+ });
284
+ window.webContents.setWindowOpenHandler(({ url }) => {
285
+ if (isSafeExternalUrl(url)) {
286
+ void shell.openExternal(url).catch((error) => {
287
+ console.error('Unable to open external URL:', error);
288
+ });
289
+ }
290
+ return { action: 'deny' };
291
+ });
292
+
293
+ if (process.env.VITE_DEV_SERVER_URL) {
294
+ void window.loadURL(process.env.VITE_DEV_SERVER_URL).catch((error) => {
295
+ console.error('Unable to load the development renderer:', error);
296
+ });
297
+ } else {
298
+ void window.loadFile(rendererIndex).catch((error) => {
299
+ console.error('Unable to load the packaged renderer:', error);
300
+ });
301
+ }
302
+ }
303
+
304
+ app.whenReady().then(() => {
305
+ session.defaultSession.setPermissionCheckHandler(() => false);
306
+ session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
307
+ createWindow();
308
+ });
309
+ app.on('window-all-closed', () => {
310
+ if (process.platform !== 'darwin') app.quit();
311
+ });
312
+ app.on('activate', () => {
313
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
314
+ });
315
+ `);
316
+ writeFile(path.join(dir, 'src/main/preload.cts'), `import { contextBridge } from 'electron';
317
+
318
+ contextBridge.exposeInMainWorld('anhedral', {
319
+ platform: process.platform,
320
+ });
321
+ `);
322
+ writeFile(path.join(dir, 'index.html'), `<!doctype html>
323
+ <html lang="en">
324
+ <head>
325
+ <meta charset="UTF-8" />
326
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
327
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'none'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https: ws://127.0.0.1:* wss:; img-src 'self' data: https:; font-src 'self' data: https:; frame-src https:; form-action 'self' https:" />
328
+ <title>${displayNameHtml}</title>
329
+ </head>
330
+ <body>
331
+ <div id="root"></div>
332
+ <script type="module" src="/src/renderer/main.tsx"></script>
333
+ </body>
334
+ </html>
335
+ `);
336
+ writeFile(path.join(dir, 'src/renderer/lib/utils.ts'), `import { clsx, type ClassValue } from 'clsx';
337
+ import { twMerge } from 'tailwind-merge';
338
+
339
+ export function cn(...inputs: ClassValue[]) {
340
+ return twMerge(clsx(inputs));
341
+ }
342
+ `);
343
+ if (options.apps.api) {
344
+ const authImport = options.features.auth ? "import { getAuthToken } from './auth';\n" : '';
345
+ const getToken = options.features.auth ? 'getAuthToken' : 'async () => null';
346
+ writeFile(path.join(dir, 'src/renderer/lib/api.ts'), `import { ApiClient, normalizeApiBaseUrl } from '@shared/api-client';
347
+ ${authImport}
348
+
349
+ function apiBaseUrl(): string {
350
+ const configured = import.meta.env.VITE_API_URL?.trim();
351
+ const candidate = configured || (import.meta.env.DEV ? 'http://localhost:8787/api' : '');
352
+ if (!candidate) throw new Error('VITE_API_URL is required in production builds');
353
+ const normalized = normalizeApiBaseUrl(candidate, 'VITE_API_URL');
354
+ const url = new URL(normalized);
355
+ if (import.meta.env.PROD && url.protocol !== 'https:') {
356
+ throw new Error('VITE_API_URL must use https: in production');
357
+ }
358
+ return normalized;
359
+ }
360
+
361
+ export const api = new ApiClient({
362
+ baseUrl: apiBaseUrl(),
363
+ getToken: ${getToken},
364
+ });
365
+ `);
366
+ }
367
+ if (options.features.auth) {
368
+ writeFile(path.join(dir, 'src/renderer/lib/auth.ts'), `import type { Clerk as ClerkInstance } from '@clerk/clerk-js';
369
+
370
+ const publishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || '';
371
+ let initialization: Promise<ClerkInstance | null> | null = null;
372
+
373
+ export function isClerkConfigured(): boolean {
374
+ return publishableKey.length > 0;
375
+ }
376
+
377
+ export function initializeClerk(): Promise<ClerkInstance | null> {
378
+ if (!publishableKey) return Promise.resolve(null);
379
+ if (!initialization) {
380
+ initialization = Promise.all([
381
+ import('@clerk/clerk-js'),
382
+ import('@clerk/ui'),
383
+ ]).then(async ([{ Clerk }, { ui }]) => {
384
+ const clerk = new Clerk(publishableKey);
385
+ await clerk.load({ ui });
386
+ return clerk;
387
+ }).catch((error) => {
388
+ initialization = null;
389
+ throw error;
390
+ });
391
+ }
392
+ return initialization;
393
+ }
394
+
395
+ export async function getAuthToken(): Promise<string | null> {
396
+ const clerk = await initializeClerk();
397
+ return clerk?.session?.getToken() ?? null;
398
+ }
399
+
400
+ export async function getAuthUserId(): Promise<string | null> {
401
+ const clerk = await initializeClerk();
402
+ return clerk?.user?.id ?? null;
403
+ }
404
+
405
+ export async function openAccount(): Promise<boolean> {
406
+ const clerk = await initializeClerk();
407
+ if (!clerk) return false;
408
+
409
+ if (clerk.user) clerk.openUserProfile();
410
+ else clerk.openSignIn();
411
+ return true;
412
+ }
413
+ `);
414
+ }
415
+ if (options.features.billing) {
416
+ writeFile(path.join(dir, 'src/renderer/hooks/use-entitlement.ts'), `import { subscribeToSubscriptionChanges } from '@shared/realtime';
417
+ import * as React from 'react';
418
+ import { api } from '../lib/api';
419
+ import { getAuthUserId, initializeClerk } from '../lib/auth';
420
+
421
+ export function useEntitlement(enabled: boolean) {
422
+ const [userId, setUserId] = React.useState<string | null>(null);
423
+ const [entitlement, setEntitlement] = React.useState<Awaited<ReturnType<typeof api.getEntitlement>> | null>(null);
424
+ const [error, setError] = React.useState<string | null>(null);
425
+ const revision = React.useRef(0);
426
+ const refresh = React.useCallback(async () => {
427
+ const currentUserId = await getAuthUserId();
428
+ setUserId(currentUserId);
429
+ if (!currentUserId) return;
430
+ try {
431
+ const next = await api.getEntitlement();
432
+ revision.current = Math.max(revision.current, next.revision);
433
+ setEntitlement(next);
434
+ setError(null);
435
+ } catch (cause) {
436
+ setError(cause instanceof Error ? cause.message : 'Unable to load subscription');
437
+ }
438
+ }, []);
439
+ React.useEffect(() => {
440
+ if (!enabled) return;
441
+ let unsubscribe: (() => void) | undefined;
442
+ void initializeClerk().then((clerk) => {
443
+ unsubscribe = clerk?.addListener(() => { void refresh(); });
444
+ void refresh();
445
+ });
446
+ return () => unsubscribe?.();
447
+ }, [enabled, refresh]);
448
+ React.useEffect(() => {
449
+ if (!enabled) return;
450
+ const onVisibility = () => { if (document.visibilityState === 'visible') void refresh(); };
451
+ document.addEventListener('visibilitychange', onVisibility);
452
+ return () => document.removeEventListener('visibilitychange', onVisibility);
453
+ }, [enabled, refresh]);
454
+ React.useEffect(() => {
455
+ if (!enabled || !userId) return;
456
+ return subscribeToSubscriptionChanges({
457
+ userId,
458
+ getTokenRequest: () => api.getRealtimeToken(),
459
+ onChange: (nextRevision) => { if (nextRevision > revision.current) void refresh(); },
460
+ onError: (cause) => setError(cause.message),
461
+ });
462
+ }, [enabled, refresh, userId]);
463
+ return { entitlement, error };
464
+ }
465
+ `);
466
+ }
467
+ writeFile(path.join(dir, 'src/renderer/components/ui/button.tsx'), `import * as React from 'react';
468
+ import { cn } from '@/lib/utils';
469
+
470
+ export function Button({ className, type = 'button', ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
471
+ return (
472
+ <button
473
+ type={type}
474
+ className={cn('inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50', className)}
475
+ {...props}
476
+ />
477
+ );
478
+ }
479
+ `);
480
+ const authImport = options.features.auth ? "import { isClerkConfigured, openAccount } from '@/lib/auth';\n" : '';
481
+ const entitlementImport = options.features.billing ? "import { useEntitlement } from '@/hooks/use-entitlement';\n" : '';
482
+ const entitlementState = options.features.billing ? " const { entitlement, error: entitlementError } = useEntitlement(clerkState === 'ready');\n" : '';
483
+ const entitlementStatus = options.features.billing ? ` {entitlement ? <p className="text-muted-foreground">Plan: {entitlement.entitlement} ({entitlement.status})</p> : null}
484
+ {entitlementError ? <p role="alert" className="text-red-700">{entitlementError}</p> : null}
485
+ ` : '';
486
+ const authState = options.features.auth ? ` const [clerkState, setClerkState] = React.useState<'unconfigured' | 'idle' | 'loading' | 'ready' | 'error'>(
487
+ () => isClerkConfigured() ? 'idle' : 'unconfigured',
488
+ );
489
+
490
+ const handleAccount = async () => {
491
+ setClerkState('loading');
492
+ try {
493
+ const opened = await openAccount();
494
+ setClerkState(opened ? 'ready' : 'unconfigured');
495
+ } catch {
496
+ setClerkState('error');
497
+ }
498
+ };
499
+
500
+ ` : '';
501
+ const authStatus = options.features.auth ? ` <p
502
+ role={clerkState === 'error' || clerkState === 'unconfigured' ? 'alert' : 'status'}
503
+ aria-live={clerkState === 'error' || clerkState === 'unconfigured' ? 'assertive' : 'polite'}
504
+ className={clerkState === 'error' || clerkState === 'unconfigured' ? 'text-red-700' : 'text-muted-foreground'}
505
+ >
506
+ {clerkState === 'unconfigured' ? 'Set VITE_CLERK_PUBLISHABLE_KEY to enable accounts.' : null}
507
+ {clerkState === 'idle' ? 'Account services load only when you open your account.' : null}
508
+ {clerkState === 'loading' ? 'Loading account services…' : null}
509
+ {clerkState === 'ready' ? 'Account services are ready.' : null}
510
+ {clerkState === 'error' ? 'Clerk could not initialize. Check the publishable key and network connection.' : null}
511
+ </p>
512
+ ` : '';
513
+ const buttonAction = options.features.auth
514
+ ? " disabled={clerkState === 'loading' || clerkState === 'unconfigured'} onClick={() => void handleAccount()}"
515
+ : '';
516
+ writeFile(path.join(dir, 'src/renderer/main.tsx'), `import React from 'react';
517
+ import ReactDOM from 'react-dom/client';
518
+ import { Button } from '@/components/ui/button';
519
+ ${authImport}${entitlementImport}import './styles.css';
520
+
521
+ function App() {
522
+ ${authState}${entitlementState} return (
523
+ <main className="flex min-h-screen flex-col gap-6 bg-background p-8 text-foreground">
524
+ <section className="flex flex-col gap-3">
525
+ <h1 className="text-3xl font-semibold">{${displayNameLiteral}}</h1>
526
+ <p className="max-w-2xl text-muted-foreground">
527
+ ${description}
528
+ </p>
529
+ </section>
530
+ ${authStatus}
531
+ ${entitlementStatus}
532
+ <Button${buttonAction}>${buttonLabel}</Button>
533
+ </main>
534
+ );
535
+ }
536
+
537
+ ReactDOM.createRoot(document.getElementById('root')!).render(
538
+ <React.StrictMode>
539
+ <App />
540
+ </React.StrictMode>,
541
+ );
542
+ `);
543
+ writeFile(path.join(dir, 'src/renderer/styles.css'), `@import "tailwindcss";
544
+
545
+ :root {
546
+ --background: oklch(1 0 0);
547
+ --foreground: oklch(0.145 0 0);
548
+ --primary: oklch(0.205 0 0);
549
+ --primary-foreground: oklch(0.985 0 0);
550
+ --muted-foreground: oklch(0.556 0 0);
551
+ }
552
+
553
+ @theme inline {
554
+ --color-background: var(--background);
555
+ --color-foreground: var(--foreground);
556
+ --color-primary: var(--primary);
557
+ --color-primary-foreground: var(--primary-foreground);
558
+ --color-muted-foreground: var(--muted-foreground);
559
+ }
560
+
561
+ body {
562
+ margin: 0;
563
+ background: var(--background);
564
+ color: var(--foreground);
565
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
566
+ }
567
+
568
+ button {
569
+ border: 0;
570
+ }
571
+ `);
572
+ }
@@ -1,2 +1,2 @@
1
1
  import type { ProjectOptions } from '../scaffold.js';
2
- export declare function scaffoldExtension(root: string, { projectName, displayName, skipInstall }: ProjectOptions): Promise<void>;
2
+ export declare function scaffoldExtension(root: string, options: ProjectOptions): Promise<void>;