anhedral 0.4.1 → 0.6.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 (85) hide show
  1. package/README.md +29 -256
  2. package/assets/anhedral-cli-init-technical.svg +322 -0
  3. package/assets/anhedral-cli-init.svg +257 -173
  4. package/dist/bin.js +28 -162
  5. package/dist/dependencies.d.ts +7 -56
  6. package/dist/dependencies.js +96 -305
  7. package/dist/index.d.ts +2 -7
  8. package/dist/index.js +1 -4
  9. package/dist/{templates → platforms}/desktop.d.ts +1 -1
  10. package/dist/{templates → platforms}/desktop.js +28 -266
  11. package/dist/{templates → platforms}/extension.d.ts +1 -1
  12. package/dist/platforms/extension.js +401 -0
  13. package/dist/{templates → platforms}/mobile.d.ts +1 -1
  14. package/dist/platforms/mobile.js +232 -0
  15. package/dist/project.d.ts +4 -0
  16. package/dist/project.js +1 -0
  17. package/dist/standard-products.d.ts +2 -0
  18. package/dist/standard-products.js +6 -0
  19. package/dist/standard.d.ts +18 -0
  20. package/dist/standard.js +578 -0
  21. package/dist/version.d.ts +1 -1
  22. package/dist/version.js +1 -1
  23. package/docs/application-stack-standard.md +700 -0
  24. package/package.json +34 -49
  25. package/CHANGELOG.md +0 -132
  26. package/CONTRIBUTING.md +0 -73
  27. package/SECURITY.md +0 -37
  28. package/SKILL.md +0 -244
  29. package/assets/images/svg/ably-symbol-color.svg +0 -8
  30. package/bin/anhedral.js +0 -2
  31. package/dist/architecture/contributions.d.ts +0 -28
  32. package/dist/architecture/contributions.js +0 -125
  33. package/dist/architecture/index.d.ts +0 -12
  34. package/dist/architecture/index.js +0 -6
  35. package/dist/architecture/manifest.d.ts +0 -53
  36. package/dist/architecture/manifest.js +0 -283
  37. package/dist/architecture/modules.d.ts +0 -37
  38. package/dist/architecture/modules.js +0 -184
  39. package/dist/architecture/plan.d.ts +0 -58
  40. package/dist/architecture/plan.js +0 -145
  41. package/dist/architecture/templates.d.ts +0 -10
  42. package/dist/architecture/templates.js +0 -23
  43. package/dist/cli.d.ts +0 -32
  44. package/dist/cli.js +0 -235
  45. package/dist/composers/root.d.ts +0 -6
  46. package/dist/composers/root.js +0 -35
  47. package/dist/print.d.ts +0 -8
  48. package/dist/print.js +0 -48
  49. package/dist/prompts.d.ts +0 -5
  50. package/dist/prompts.js +0 -19
  51. package/dist/scaffold.d.ts +0 -76
  52. package/dist/scaffold.js +0 -1895
  53. package/dist/template-source.d.ts +0 -20
  54. package/dist/template-source.js +0 -180
  55. package/dist/templates/api.d.ts +0 -2
  56. package/dist/templates/api.js +0 -3122
  57. package/dist/templates/assets-private-proxy.d.ts +0 -3
  58. package/dist/templates/assets-private-proxy.js +0 -277
  59. package/dist/templates/electron-updater.d.ts +0 -3
  60. package/dist/templates/electron-updater.js +0 -358
  61. package/dist/templates/extension.js +0 -732
  62. package/dist/templates/mobile.js +0 -720
  63. package/dist/templates/shared.d.ts +0 -2
  64. package/dist/templates/shared.js +0 -520
  65. package/dist/templates/web.d.ts +0 -2
  66. package/dist/templates/web.js +0 -412
  67. package/dist/toolchain.d.ts +0 -3
  68. package/dist/toolchain.js +0 -8
  69. package/dist/ui.d.ts +0 -33
  70. package/dist/ui.js +0 -199
  71. package/dist/workspace-config.d.ts +0 -11
  72. package/dist/workspace-config.js +0 -372
  73. package/docs/conventions.md +0 -92
  74. package/docs/master-stack-map.md +0 -199
  75. package/docs/output-tree-contract.md +0 -482
  76. package/docs/references/manual-scaffolding.md +0 -403
  77. package/docs/references/provisioning.md +0 -220
  78. package/scripts/render-master-stack-map.mjs +0 -510
  79. package/templates/api-fastify/apps/api/tsconfig.json +0 -20
  80. package/templates/catalog.json +0 -23
  81. package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
  82. package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
  83. package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
  84. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
  85. package/templates/web-next/apps/web/next-env.d.ts +0 -6
@@ -1,48 +1,21 @@
1
1
  import path from 'node:path';
2
2
  import { writeFile } from '../util.js';
3
- import { anhedralPrint } from '../print.js';
4
- import { DESKTOP_DEPENDENCIES, ELECTRON_UPDATER_DEPENDENCIES } from '../dependencies.js';
3
+ import { DESKTOP_DEPENDENCIES } from '../dependencies.js';
5
4
  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
- if (options.features.electronUpdater) {
18
- Object.assign(dependencies, ELECTRON_UPDATER_DEPENDENCIES.dependencies);
19
- }
20
- return dependencies;
21
- }
22
5
  export async function scaffoldDesktop(root, options) {
23
6
  const { projectName, displayName } = options;
24
7
  const dir = path.join(root, 'apps/desktop');
25
- anhedralPrint.section('Desktop (Electron + shadcn/ui)');
26
- anhedralPrint.step('Writing Electron desktop app');
27
- writePackageJson(dir, projectName, options);
8
+ writePackageJson(dir, projectName);
28
9
  writeTsConfig(dir);
29
10
  writeViteConfig(dir);
30
11
  writePostcssConfig(dir);
31
12
  writeDevScript(dir);
32
13
  writeShadcnConfig(dir);
33
- writeEnvExample(dir, options);
34
- writeSourceFiles(dir, displayName, options);
35
- anhedralPrint.done('Electron desktop app written');
14
+ writeEnvExample(dir);
15
+ writeSourceFiles(dir, displayName);
36
16
  }
37
- function writePackageJson(dir, projectName, options) {
38
- const updatePublish = options.features.electronUpdater ? {
39
- artifactName: '${productName}-${version}-${os}-${arch}.${ext}',
40
- publish: [{
41
- provider: 'generic',
42
- url: '${env.DESKTOP_UPDATE_BASE_URL}/releases/${os}/${arch}',
43
- useMultipleRangeRequest: false,
44
- }],
45
- } : {};
17
+ function writePackageJson(dir, projectName) {
18
+ const updatePublish = {};
46
19
  writeFile(path.join(dir, 'package.json'), JSON.stringify({
47
20
  name: childPackageName(projectName, 'desktop'),
48
21
  version: '0.1.0',
@@ -53,15 +26,11 @@ function writePackageJson(dir, projectName, options) {
53
26
  dev: 'tsc -p tsconfig.main.json && node scripts/dev.mjs',
54
27
  build: 'tsc --noEmit && tsc -p tsconfig.main.json && vite build',
55
28
  typecheck: 'tsc --noEmit',
56
- 'build:mac': `pnpm build && electron-builder --mac${options.features.electronUpdater ? ' --publish never' : ''}`,
57
- 'build:win': `pnpm build && electron-builder --win${options.features.electronUpdater ? ' --publish never' : ''}`,
58
- 'build:linux': `pnpm build && electron-builder --linux${options.features.electronUpdater ? ' --publish never' : ''}`,
59
- package: `pnpm build && electron-builder${options.features.electronUpdater ? ' --publish never' : ''}`,
60
- ...(options.features.electronUpdater ? {
61
- 'updates:build:mac': 'pnpm build && electron-builder --mac --publish never',
62
- 'updates:build:win': 'pnpm build && electron-builder --win --publish never',
63
- 'updates:build:linux': 'pnpm build && electron-builder --linux --publish never',
64
- } : {}),
29
+ 'build:mac': `pnpm build && electron-builder --mac${''}`,
30
+ 'build:win': `pnpm build && electron-builder --win${''}`,
31
+ 'build:linux': `pnpm build && electron-builder --linux${''}`,
32
+ package: `pnpm build && electron-builder${''}`,
33
+ ...({}),
65
34
  },
66
35
  build: {
67
36
  appId: `dev.anhedral.${identifierSegment(projectName)}`,
@@ -85,7 +54,7 @@ function writePackageJson(dir, projectName, options) {
85
54
  },
86
55
  ...updatePublish,
87
56
  },
88
- dependencies: selectedDependencies(options),
57
+ dependencies: DESKTOP_DEPENDENCIES.dependencies,
89
58
  devDependencies: DESKTOP_DEPENDENCIES.devDependencies,
90
59
  }, null, 2) + '\n');
91
60
  }
@@ -121,6 +90,9 @@ export default defineConfig({
121
90
  build: {
122
91
  outDir: 'dist/renderer',
123
92
  emptyOutDir: true,
93
+ // Clerk is already loaded behind a dynamic import. Its optional account-management
94
+ // surface is intentionally a large async chunk and does not inflate initial rendering.
95
+ chunkSizeWarningLimit: 1600,
124
96
  },
125
97
  });
126
98
  `);
@@ -237,62 +209,12 @@ function writeShadcnConfig(dir) {
237
209
  iconLibrary: 'lucide',
238
210
  }, null, 2) + '\n');
239
211
  }
240
- function writeEnvExample(dir, options) {
241
- const lines = [
242
- options.apps.api ? 'VITE_API_URL=http://localhost:8787/api' : null,
243
- options.features.auth ? 'VITE_CLERK_PUBLISHABLE_KEY=pk_test_***' : null,
244
- ].filter((value) => value !== null);
245
- writeFile(path.join(dir, '.env.example'), lines.join('\n') + (lines.length > 0 ? '\n' : ''));
246
- if (options.features.electronUpdater) {
247
- writeFile(path.join(dir, 'electron-builder.env.example'), 'DESKTOP_UPDATE_BASE_URL=https://updates.example.com\n');
248
- }
212
+ function writeEnvExample(dir) {
213
+ writeFile(path.join(dir, '.env.example'), '');
249
214
  }
250
- function writeSourceFiles(dir, displayName, options) {
215
+ function writeSourceFiles(dir, displayName) {
251
216
  const displayNameLiteral = jsString(displayName);
252
217
  const displayNameHtml = htmlText(displayName);
253
- const description = options.apps.api
254
- ? 'Electron + shadcn/ui desktop client using the same shared API client as web, mobile, and extension.'
255
- : 'Electron + shadcn/ui desktop client ready for your application.';
256
- const buttonLabel = options.features.auth ? 'Open account' : options.apps.api ? 'Explore API' : 'Get started';
257
- const updaterImport = options.features.electronUpdater
258
- ? "import electronUpdater, { type AppUpdater } from 'electron-updater';\n"
259
- : '';
260
- const updaterRuntime = options.features.electronUpdater ? `
261
- const UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
262
- let updateTimer: NodeJS.Timeout | undefined;
263
-
264
- function getAutoUpdater(): AppUpdater {
265
- const { autoUpdater } = electronUpdater;
266
- return autoUpdater;
267
- }
268
-
269
- async function checkForDesktopUpdate(updater: AppUpdater): Promise<void> {
270
- try {
271
- await updater.checkForUpdatesAndNotify();
272
- } catch (error) {
273
- console.error('Desktop update check failed:', error);
274
- }
275
- }
276
-
277
- function startDesktopUpdates(): void {
278
- if (!app.isPackaged) return;
279
- const updater = getAutoUpdater();
280
- updater.autoDownload = true;
281
- updater.autoInstallOnAppQuit = true;
282
- updater.on('error', (error) => {
283
- console.error('Desktop updater error:', error);
284
- });
285
- void checkForDesktopUpdate(updater);
286
- updateTimer = setInterval(() => {
287
- void checkForDesktopUpdate(updater);
288
- }, UPDATE_CHECK_INTERVAL_MS);
289
- updateTimer.unref();
290
- }
291
- ` : '';
292
- const updaterStart = options.features.electronUpdater ? ' startDesktopUpdates();\n' : '';
293
- const updaterStop = options.features.electronUpdater
294
- ? "app.on('before-quit', () => { if (updateTimer) clearInterval(updateTimer); });\n"
295
- : '';
296
218
  writeFile(path.join(dir, 'src/main/app-window.ts'), `import { BrowserWindow, shell } from 'electron';
297
219
  import path from 'node:path';
298
220
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -366,20 +288,20 @@ export function createAppWindow(): BrowserWindow {
366
288
  `);
367
289
  writeFile(path.join(dir, 'src/main/main.ts'), `import { app, BrowserWindow, session } from 'electron';
368
290
  import { createAppWindow } from './app-window.js';
369
- ${updaterImport}${updaterRuntime}
291
+
370
292
 
371
293
  app.whenReady().then(() => {
372
294
  session.defaultSession.setPermissionCheckHandler(() => false);
373
295
  session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
374
296
  createAppWindow();
375
- ${updaterStart}});
297
+ });
376
298
  app.on('window-all-closed', () => {
377
299
  if (process.platform !== 'darwin') app.quit();
378
300
  });
379
301
  app.on('activate', () => {
380
302
  if (BrowserWindow.getAllWindows().length === 0) createAppWindow();
381
303
  });
382
- ${updaterStop}`);
304
+ `);
383
305
  writeFile(path.join(dir, 'src/main/preload.cts'), `import { contextBridge } from 'electron';
384
306
 
385
307
  contextBridge.exposeInMainWorld('anhedral', {
@@ -407,130 +329,6 @@ export function cn(...inputs: ClassValue[]) {
407
329
  return twMerge(clsx(inputs));
408
330
  }
409
331
  `);
410
- if (options.apps.api) {
411
- const authImport = options.features.auth ? "import { getAuthToken } from './auth';\n" : '';
412
- const getToken = options.features.auth ? 'getAuthToken' : 'async () => null';
413
- writeFile(path.join(dir, 'src/renderer/lib/api.ts'), `import { ApiClient, normalizeApiBaseUrl } from '@shared/api-client';
414
- ${authImport}
415
-
416
- function apiBaseUrl(): string {
417
- const configured = import.meta.env.VITE_API_URL?.trim();
418
- const candidate = configured || (import.meta.env.DEV ? 'http://localhost:8787/api' : '');
419
- if (!candidate) throw new Error('VITE_API_URL is required in production builds');
420
- const normalized = normalizeApiBaseUrl(candidate, 'VITE_API_URL');
421
- const url = new URL(normalized);
422
- if (import.meta.env.PROD && url.protocol !== 'https:') {
423
- throw new Error('VITE_API_URL must use https: in production');
424
- }
425
- return normalized;
426
- }
427
-
428
- export const api = new ApiClient({
429
- baseUrl: apiBaseUrl(),
430
- getToken: ${getToken},
431
- });
432
- `);
433
- }
434
- if (options.features.auth) {
435
- writeFile(path.join(dir, 'src/renderer/lib/auth.ts'), `import type { Clerk as ClerkInstance } from '@clerk/clerk-js';
436
-
437
- const publishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || '';
438
- let initialization: Promise<ClerkInstance | null> | null = null;
439
-
440
- export function isClerkConfigured(): boolean {
441
- return publishableKey.length > 0;
442
- }
443
-
444
- export function initializeClerk(): Promise<ClerkInstance | null> {
445
- if (!publishableKey) return Promise.resolve(null);
446
- if (!initialization) {
447
- initialization = Promise.all([
448
- import('@clerk/clerk-js'),
449
- import('@clerk/ui'),
450
- ]).then(async ([{ Clerk }, { ui }]) => {
451
- const clerk = new Clerk(publishableKey);
452
- await clerk.load({ ui });
453
- return clerk;
454
- }).catch((error) => {
455
- initialization = null;
456
- throw error;
457
- });
458
- }
459
- return initialization;
460
- }
461
-
462
- export async function getAuthToken(): Promise<string | null> {
463
- const clerk = await initializeClerk();
464
- return clerk?.session?.getToken() ?? null;
465
- }
466
-
467
- export async function getAuthUserId(): Promise<string | null> {
468
- const clerk = await initializeClerk();
469
- return clerk?.user?.id ?? null;
470
- }
471
-
472
- export async function openAccount(): Promise<boolean> {
473
- const clerk = await initializeClerk();
474
- if (!clerk) return false;
475
-
476
- if (clerk.user) clerk.openUserProfile();
477
- else clerk.openSignIn();
478
- return true;
479
- }
480
- `);
481
- }
482
- if (options.features.billing) {
483
- writeFile(path.join(dir, 'src/renderer/hooks/use-entitlement.ts'), `import { subscribeToSubscriptionChanges } from '@shared/realtime';
484
- import * as React from 'react';
485
- import { api } from '../lib/api';
486
- import { getAuthUserId, initializeClerk } from '../lib/auth';
487
-
488
- export function useEntitlement(enabled: boolean) {
489
- const [userId, setUserId] = React.useState<string | null>(null);
490
- const [entitlement, setEntitlement] = React.useState<Awaited<ReturnType<typeof api.getEntitlement>> | null>(null);
491
- const [error, setError] = React.useState<string | null>(null);
492
- const revision = React.useRef(0);
493
- const refresh = React.useCallback(async () => {
494
- const currentUserId = await getAuthUserId();
495
- setUserId(currentUserId);
496
- if (!currentUserId) return;
497
- try {
498
- const next = await api.getEntitlement();
499
- revision.current = Math.max(revision.current, next.revision);
500
- setEntitlement(next);
501
- setError(null);
502
- } catch (cause) {
503
- setError(cause instanceof Error ? cause.message : 'Unable to load subscription');
504
- }
505
- }, []);
506
- React.useEffect(() => {
507
- if (!enabled) return;
508
- let unsubscribe: (() => void) | undefined;
509
- void initializeClerk().then((clerk) => {
510
- unsubscribe = clerk?.addListener(() => { void refresh(); });
511
- void refresh();
512
- });
513
- return () => unsubscribe?.();
514
- }, [enabled, refresh]);
515
- React.useEffect(() => {
516
- if (!enabled) return;
517
- const onVisibility = () => { if (document.visibilityState === 'visible') void refresh(); };
518
- document.addEventListener('visibilitychange', onVisibility);
519
- return () => document.removeEventListener('visibilitychange', onVisibility);
520
- }, [enabled, refresh]);
521
- React.useEffect(() => {
522
- if (!enabled || !userId) return;
523
- return subscribeToSubscriptionChanges({
524
- userId,
525
- getTokenRequest: () => api.getRealtimeToken(),
526
- onChange: (nextRevision) => { if (nextRevision > revision.current) void refresh(); },
527
- onError: (cause) => setError(cause.message),
528
- });
529
- }, [enabled, refresh, userId]);
530
- return { entitlement, error };
531
- }
532
- `);
533
- }
534
332
  writeFile(path.join(dir, 'src/renderer/components/ui/button.tsx'), `import * as React from 'react';
535
333
  import { cn } from '@/lib/utils';
536
334
 
@@ -544,59 +342,23 @@ export function Button({ className, type = 'button', ...props }: React.ButtonHTM
544
342
  );
545
343
  }
546
344
  `);
547
- const authImport = options.features.auth ? "import { isClerkConfigured, openAccount } from '@/lib/auth';\n" : '';
548
- const entitlementImport = options.features.billing ? "import { useEntitlement } from '@/hooks/use-entitlement';\n" : '';
549
- const entitlementState = options.features.billing ? " const { entitlement, error: entitlementError } = useEntitlement(clerkState === 'ready');\n" : '';
550
- const entitlementStatus = options.features.billing ? ` {entitlement ? <p className="text-muted-foreground">Plan: {entitlement.entitlement} ({entitlement.status})</p> : null}
551
- {entitlementError ? <p role="alert" className="text-red-700">{entitlementError}</p> : null}
552
- ` : '';
553
- const authState = options.features.auth ? ` const [clerkState, setClerkState] = React.useState<'unconfigured' | 'idle' | 'loading' | 'ready' | 'error'>(
554
- () => isClerkConfigured() ? 'idle' : 'unconfigured',
555
- );
556
-
557
- const handleAccount = async () => {
558
- setClerkState('loading');
559
- try {
560
- const opened = await openAccount();
561
- setClerkState(opened ? 'ready' : 'unconfigured');
562
- } catch {
563
- setClerkState('error');
564
- }
565
- };
566
-
567
- ` : '';
568
- const authStatus = options.features.auth ? ` <p
569
- role={clerkState === 'error' || clerkState === 'unconfigured' ? 'alert' : 'status'}
570
- aria-live={clerkState === 'error' || clerkState === 'unconfigured' ? 'assertive' : 'polite'}
571
- className={clerkState === 'error' || clerkState === 'unconfigured' ? 'text-red-700' : 'text-muted-foreground'}
572
- >
573
- {clerkState === 'unconfigured' ? 'Set VITE_CLERK_PUBLISHABLE_KEY to enable accounts.' : null}
574
- {clerkState === 'idle' ? 'Account services load only when you open your account.' : null}
575
- {clerkState === 'loading' ? 'Loading account services…' : null}
576
- {clerkState === 'ready' ? 'Account services are ready.' : null}
577
- {clerkState === 'error' ? 'Clerk could not initialize. Check the publishable key and network connection.' : null}
578
- </p>
579
- ` : '';
580
- const buttonAction = options.features.auth
581
- ? " disabled={clerkState === 'loading' || clerkState === 'unconfigured'} onClick={() => void handleAccount()}"
582
- : '';
583
345
  writeFile(path.join(dir, 'src/renderer/main.tsx'), `import React from 'react';
584
346
  import ReactDOM from 'react-dom/client';
585
- import { Button } from '@/components/ui/button';
586
- ${authImport}${entitlementImport}import './styles.css';
347
+ import './styles.css';
587
348
 
588
349
  function App() {
589
- ${authState}${entitlementState} return (
350
+ return (
590
351
  <main className="flex min-h-screen flex-col gap-6 bg-background p-8 text-foreground">
591
352
  <section className="flex flex-col gap-3">
592
353
  <h1 className="text-3xl font-semibold">{${displayNameLiteral}}</h1>
593
354
  <p className="max-w-2xl text-muted-foreground">
594
- ${description}
355
+ ${"Electron + shadcn/ui desktop client ready for your application."}
595
356
  </p>
596
357
  </section>
597
- ${authStatus}
598
- ${entitlementStatus}
599
- <Button${buttonAction}>${buttonLabel}</Button>
358
+
359
+
360
+
361
+
600
362
  </main>
601
363
  );
602
364
  }
@@ -1,2 +1,2 @@
1
- import type { ProjectOptions } from '../scaffold.js';
1
+ import type { ProjectOptions } from '../project.js';
2
2
  export declare function scaffoldExtension(root: string, options: ProjectOptions): Promise<void>;