anhedral 0.4.1 → 0.4.2

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 (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +68 -55
  4. package/SKILL.md +107 -12
  5. package/dist/architecture/index.d.ts +2 -2
  6. package/dist/architecture/index.js +1 -1
  7. package/dist/architecture/modules.d.ts +5 -1
  8. package/dist/architecture/modules.js +4 -1
  9. package/dist/bin.js +113 -84
  10. package/dist/cli.d.ts +7 -7
  11. package/dist/cli.js +62 -16
  12. package/dist/dependencies.d.ts +1 -0
  13. package/dist/dependencies.js +11 -8
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +1 -1
  16. package/dist/project.d.ts +30 -0
  17. package/dist/project.js +1 -0
  18. package/dist/prompts.d.ts +4 -3
  19. package/dist/prompts.js +29 -7
  20. package/dist/scaffold.d.ts +12 -24
  21. package/dist/scaffold.js +330 -123
  22. package/dist/templates/api.d.ts +1 -1
  23. package/dist/templates/api.js +67 -2
  24. package/dist/templates/assets-private-proxy.d.ts +1 -1
  25. package/dist/templates/desktop.d.ts +1 -1
  26. package/dist/templates/desktop.js +189 -36
  27. package/dist/templates/electron-updater.d.ts +1 -1
  28. package/dist/templates/electron-updater.js +1 -1
  29. package/dist/templates/extension.d.ts +1 -1
  30. package/dist/templates/extension.js +138 -14
  31. package/dist/templates/first-run.d.ts +2 -0
  32. package/dist/templates/first-run.js +167 -0
  33. package/dist/templates/mobile.d.ts +1 -1
  34. package/dist/templates/mobile.js +158 -17
  35. package/dist/templates/shared.d.ts +1 -1
  36. package/dist/templates/shared.js +64 -7
  37. package/dist/templates/web.d.ts +1 -1
  38. package/dist/templates/web.js +185 -28
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/docs/NORTHSTAR.md +114 -0
  42. package/docs/conventions.md +6 -4
  43. package/docs/engineering-principles.md +88 -0
  44. package/docs/output-tree-contract.md +16 -5
  45. package/docs/references/manual-scaffolding.md +22 -4
  46. package/docs/references/provisioning.md +434 -77
  47. package/package.json +3 -1
@@ -1,2 +1,2 @@
1
- import type { ProjectOptions } from '../scaffold.js';
1
+ import type { ProjectOptions } from '../project.js';
2
2
  export declare function scaffoldApi(root: string, options: ProjectOptions): Promise<void>;
@@ -1635,6 +1635,9 @@ function routesSource(options) {
1635
1635
  const storageService = options.features.storage
1636
1636
  ? ' const storageService = dependencies.storageService ?? createStorageService(env);\n'
1637
1637
  : '';
1638
+ const appRoutesRegistration = options.features.database && options.features.auth
1639
+ ? 'await app.register(appRoutes(env));'
1640
+ : 'await app.register(appRoutes);';
1638
1641
  return `${imports.join('\n')}
1639
1642
  import type { AppEnv } from './env';
1640
1643
 
@@ -1668,7 +1671,7 @@ export function routes(env: AppEnv, dependencies: RouteDependencies = {}): Fasti
1668
1671
  ${billingStore}${storageService} const serviceState = dependencies.serviceState ?? { readiness: async () => 'ready' as const };
1669
1672
  ${routes.join('\n\n ')}
1670
1673
 
1671
- await app.register(appRoutes);
1674
+ ${appRoutesRegistration}
1672
1675
  };
1673
1676
  }
1674
1677
  `;
@@ -2944,7 +2947,69 @@ export default defineConfig({
2944
2947
  writeFile(path.join(dir, 'src/storage.ts'), storageSource());
2945
2948
  if (options.features.billing)
2946
2949
  writeFile(path.join(dir, 'src/billing.ts'), billingSource());
2947
- writeFile(path.join(dir, 'src/routes/app.ts'), `import type { FastifyPluginAsync } from 'fastify';
2950
+ const appRouteAuthImports = options.features.auth
2951
+ ? `import { authenticatedUserId } from '../auth';
2952
+ import type { AppEnv } from '../env';
2953
+ `
2954
+ : '';
2955
+ const appRouteRegistrationStart = options.features.auth
2956
+ ? `export function appRoutes(env: AppEnv): FastifyPluginAsync {
2957
+ return async function registerAppRoutes(app) {`
2958
+ : 'export const appRoutes: FastifyPluginAsync = async (app) => {';
2959
+ const appRouteRegistrationEnd = options.features.auth ? ' };\n}' : '};';
2960
+ const listOwner = options.features.auth
2961
+ ? ` const userId = authenticatedUserId(request, env);
2962
+ const rows = await db.select().from(items)
2963
+ .where(eq(items.userId, userId))
2964
+ .orderBy(desc(items.createdAt))
2965
+ .limit(50);`
2966
+ : ` const rows = await db.select().from(items)
2967
+ .orderBy(desc(items.createdAt))
2968
+ .limit(50);`;
2969
+ const createOwner = options.features.auth
2970
+ ? ' userId: authenticatedUserId(request, env),\n'
2971
+ : '';
2972
+ writeFile(path.join(dir, 'src/routes/app.ts'), options.features.database
2973
+ ? `import { randomUUID } from 'node:crypto';
2974
+ import type { FastifyPluginAsync } from 'fastify';
2975
+ import { CreateItemRequestSchema, ItemListSchema, ItemSchema } from '@shared/contracts';
2976
+ import { db } from '@shared/db';
2977
+ import { items } from '@shared/db/schema';
2978
+ import { desc${options.features.auth ? ', eq' : ''} } from 'drizzle-orm';
2979
+ ${appRouteAuthImports}
2980
+
2981
+ // A complete starter feature. Keep the boundary or replace it with your product model.
2982
+ ${appRouteRegistrationStart}
2983
+ app.get('/items', async (request) => {
2984
+ ${listOwner}
2985
+ return ItemListSchema.parse(rows.map((item) => ({
2986
+ ...item,
2987
+ createdAt: item.createdAt.toISOString(),
2988
+ })));
2989
+ });
2990
+
2991
+ app.post('/items', async (request, reply) => {
2992
+ const parsed = CreateItemRequestSchema.safeParse(request.body);
2993
+ if (!parsed.success) {
2994
+ return reply.code(400).send({
2995
+ error: 'invalid_item_request',
2996
+ message: 'Item name must contain 1 to 120 characters',
2997
+ details: parsed.error.flatten(),
2998
+ });
2999
+ }
3000
+ const [created] = await db.insert(items).values({
3001
+ id: randomUUID(),
3002
+ ${createOwner} name: parsed.data.name,
3003
+ }).returning();
3004
+ if (!created) throw new Error('Database did not return the created item');
3005
+ return reply.code(201).send(ItemSchema.parse({
3006
+ ...created,
3007
+ createdAt: created.createdAt.toISOString(),
3008
+ }));
3009
+ });
3010
+ ${appRouteRegistrationEnd}
3011
+ `
3012
+ : `import type { FastifyPluginAsync } from 'fastify';
2948
3013
 
2949
3014
  // Register product-owned routes here. Anhedral never rewrites this file after creation.
2950
3015
  export const appRoutes: FastifyPluginAsync = async (_app) => {};
@@ -1,3 +1,3 @@
1
- import type { ProjectOptions } from '../scaffold.js';
1
+ import type { ProjectOptions } from '../project.js';
2
2
  export declare function r2BucketName(projectName: string): string;
3
3
  export declare function scaffoldAssetsPrivateProxy(root: string, options: ProjectOptions): void;
@@ -1,2 +1,2 @@
1
- import type { ProjectOptions } from '../scaffold.js';
1
+ import type { ProjectOptions } from '../project.js';
2
2
  export declare function scaffoldDesktop(root: string, options: ProjectOptions): Promise<void>;
@@ -13,6 +13,7 @@ function selectedDependencies(options) {
13
13
  delete dependencies['@clerk/clerk-js'];
14
14
  delete dependencies['@clerk/ui'];
15
15
  delete dependencies['@solana/web3.js'];
16
+ delete dependencies.bs58;
16
17
  }
17
18
  if (options.features.electronUpdater) {
18
19
  Object.assign(dependencies, ELECTRON_UPDATER_DEPENDENCIES.dependencies);
@@ -253,7 +254,6 @@ function writeSourceFiles(dir, displayName, options) {
253
254
  const description = options.apps.api
254
255
  ? 'Electron + shadcn/ui desktop client using the same shared API client as web, mobile, and extension.'
255
256
  : '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
257
  const updaterImport = options.features.electronUpdater
258
258
  ? "import electronUpdater, { type AppUpdater } from 'electron-updater';\n"
259
259
  : '';
@@ -438,11 +438,11 @@ const publishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || '';
438
438
  let initialization: Promise<ClerkInstance | null> | null = null;
439
439
 
440
440
  export function isClerkConfigured(): boolean {
441
- return publishableKey.length > 0;
441
+ return publishableKey.length > 0 && !publishableKey.includes('***');
442
442
  }
443
443
 
444
444
  export function initializeClerk(): Promise<ClerkInstance | null> {
445
- if (!publishableKey) return Promise.resolve(null);
445
+ if (!isClerkConfigured()) return Promise.resolve(null);
446
446
  if (!initialization) {
447
447
  initialization = Promise.all([
448
448
  import('@clerk/clerk-js'),
@@ -469,6 +469,17 @@ export async function getAuthUserId(): Promise<string | null> {
469
469
  return clerk?.user?.id ?? null;
470
470
  }
471
471
 
472
+ export async function subscribeToAuthState(listener: (userId: string | null) => void): Promise<() => void> {
473
+ const clerk = await initializeClerk();
474
+ if (!clerk) {
475
+ listener(null);
476
+ return () => undefined;
477
+ }
478
+ const synchronize = () => listener(clerk.user?.id ?? null);
479
+ synchronize();
480
+ return clerk.addListener(synchronize);
481
+ }
482
+
472
483
  export async function openAccount(): Promise<boolean> {
473
484
  const clerk = await initializeClerk();
474
485
  if (!clerk) return false;
@@ -483,51 +494,59 @@ export async function openAccount(): Promise<boolean> {
483
494
  writeFile(path.join(dir, 'src/renderer/hooks/use-entitlement.ts'), `import { subscribeToSubscriptionChanges } from '@shared/realtime';
484
495
  import * as React from 'react';
485
496
  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);
497
+ export function useEntitlement(identity: string | null) {
490
498
  const [entitlement, setEntitlement] = React.useState<Awaited<ReturnType<typeof api.getEntitlement>> | null>(null);
499
+ const [loadedIdentity, setLoadedIdentity] = React.useState<string | null>(null);
491
500
  const [error, setError] = React.useState<string | null>(null);
492
501
  const revision = React.useRef(0);
502
+ const identityRef = React.useRef(identity);
493
503
  const refresh = React.useCallback(async () => {
494
- const currentUserId = await getAuthUserId();
495
- setUserId(currentUserId);
496
- if (!currentUserId) return;
504
+ if (!identity) return;
497
505
  try {
498
506
  const next = await api.getEntitlement();
507
+ if (identityRef.current !== identity) return;
499
508
  revision.current = Math.max(revision.current, next.revision);
500
509
  setEntitlement(next);
510
+ setLoadedIdentity(identity);
501
511
  setError(null);
502
512
  } catch (cause) {
513
+ if (identityRef.current !== identity) return;
514
+ setLoadedIdentity(identity);
503
515
  setError(cause instanceof Error ? cause.message : 'Unable to load subscription');
504
516
  }
505
- }, []);
517
+ }, [identity]);
506
518
  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]);
519
+ identityRef.current = identity;
520
+ revision.current = 0;
521
+ setEntitlement(null);
522
+ setLoadedIdentity(null);
523
+ setError(null);
524
+ void refresh();
525
+ }, [identity, refresh]);
515
526
  React.useEffect(() => {
516
- if (!enabled) return;
527
+ if (!identity) return;
517
528
  const onVisibility = () => { if (document.visibilityState === 'visible') void refresh(); };
518
529
  document.addEventListener('visibilitychange', onVisibility);
519
530
  return () => document.removeEventListener('visibilitychange', onVisibility);
520
- }, [enabled, refresh]);
531
+ }, [identity, refresh]);
521
532
  React.useEffect(() => {
522
- if (!enabled || !userId) return;
533
+ if (!identity) return;
523
534
  return subscribeToSubscriptionChanges({
524
- userId,
535
+ userId: identity,
525
536
  getTokenRequest: () => api.getRealtimeToken(),
526
537
  onChange: (nextRevision) => { if (nextRevision > revision.current) void refresh(); },
527
- onError: (cause) => setError(cause.message),
538
+ onError: (cause) => {
539
+ if (identityRef.current === identity) {
540
+ setLoadedIdentity(identity);
541
+ setError(cause.message);
542
+ }
543
+ },
528
544
  });
529
- }, [enabled, refresh, userId]);
530
- return { entitlement, error };
545
+ }, [identity, refresh]);
546
+ return {
547
+ entitlement: loadedIdentity === identity ? entitlement : null,
548
+ error: loadedIdentity === identity ? error : null,
549
+ };
531
550
  }
532
551
  `);
533
552
  }
@@ -544,22 +563,147 @@ export function Button({ className, type = 'button', ...props }: React.ButtonHTM
544
563
  );
545
564
  }
546
565
  `);
547
- const authImport = options.features.auth ? "import { isClerkConfigured, openAccount } from '@/lib/auth';\n" : '';
566
+ if (options.apps.api && options.features.database) {
567
+ writeFile(path.join(dir, 'src/renderer/components/item-list.tsx'), `import { createItem, listItems, type Item } from '@shared/api-client';
568
+ import * as React from 'react';
569
+ import { api } from '@/lib/api';
570
+ import { Button } from '@/components/ui/button';
571
+
572
+ export function ItemList({ identity = 'public' }: { identity?: string | null }) {
573
+ const [items, setItems] = React.useState<Item[]>([]);
574
+ const [loadedIdentity, setLoadedIdentity] = React.useState<string | null>(null);
575
+ const [name, setName] = React.useState('');
576
+ const [status, setStatus] = React.useState<'idle' | 'loading' | 'ready' | 'saving'>('idle');
577
+ const [error, setError] = React.useState<string | null>(null);
578
+ const identityRef = React.useRef(identity);
579
+ const visibleItems = loadedIdentity === identity ? items : [];
580
+ const visibleError = loadedIdentity === identity ? error : null;
581
+ const isLoading = status === 'loading' || loadedIdentity !== identity;
582
+
583
+ React.useEffect(() => {
584
+ identityRef.current = identity;
585
+ if (!identity) return;
586
+ let active = true;
587
+ setLoadedIdentity(identity);
588
+ setItems([]);
589
+ setName('');
590
+ setError(null);
591
+ setStatus('loading');
592
+ void listItems(api).then((nextItems) => {
593
+ if (active) setItems(nextItems);
594
+ }).catch((cause: unknown) => {
595
+ if (active) setError(cause instanceof Error ? cause.message : 'Unable to load items');
596
+ }).finally(() => {
597
+ if (active) setStatus('ready');
598
+ });
599
+ return () => { active = false; };
600
+ }, [identity]);
601
+
602
+ async function submit(event: React.FormEvent<HTMLFormElement>) {
603
+ event.preventDefault();
604
+ const nextName = name.trim();
605
+ if (!identity || !nextName || status !== 'ready' || loadedIdentity !== identity) return;
606
+ const submittedIdentity = identity;
607
+ setStatus('saving');
608
+ try {
609
+ const created = await createItem(api, { name: nextName });
610
+ if (identityRef.current !== submittedIdentity) return;
611
+ setItems((current) => [created, ...current]);
612
+ setName('');
613
+ setError(null);
614
+ } catch (cause) {
615
+ if (identityRef.current === submittedIdentity) {
616
+ setError(cause instanceof Error ? cause.message : 'Unable to create item');
617
+ }
618
+ } finally {
619
+ if (identityRef.current === submittedIdentity) setStatus('ready');
620
+ }
621
+ }
622
+
623
+ if (!identity) {
624
+ return <p className="text-muted-foreground">Open your account and sign in to use the working starter feature.</p>;
625
+ }
626
+
627
+ return (
628
+ <section aria-labelledby="starter-feature-title" className="flex max-w-2xl flex-col gap-3 rounded-lg border border-slate-200 bg-white p-5">
629
+ <h2 className="text-xl font-semibold" id="starter-feature-title">Working starter feature</h2>
630
+ <form className="flex gap-2" onSubmit={(event) => void submit(event)}>
631
+ <label className="sr-only" htmlFor="item-name">Item name</label>
632
+ <input
633
+ className="h-9 flex-1 rounded-md border border-slate-300 bg-background px-3 text-sm"
634
+ id="item-name"
635
+ maxLength={120}
636
+ onChange={(event) => setName(event.target.value)}
637
+ placeholder="Your first item"
638
+ value={name}
639
+ />
640
+ <Button disabled={status !== 'ready' || loadedIdentity !== identity || !name.trim()} type="submit">
641
+ {status === 'saving' ? 'Adding…' : 'Add item'}
642
+ </Button>
643
+ </form>
644
+ {visibleError ? (
645
+ <p className="text-sm text-red-700" role="alert">
646
+ {visibleError}. Check DATABASE_URL, run pnpm db:migrate, and make sure the API is running.
647
+ </p>
648
+ ) : null}
649
+ {isLoading ? <p className="text-sm text-muted-foreground">Loading items…</p> : null}
650
+ {!isLoading && visibleItems.length === 0 && !visibleError
651
+ ? <p className="text-sm text-muted-foreground">Your database is connected. Add the first item.</p>
652
+ : null}
653
+ {visibleItems.length > 0 ? (
654
+ <ul className="divide-y rounded-md border border-slate-200">
655
+ {visibleItems.map((item) => <li className="px-3 py-2 text-sm" key={item.id}>{item.name}</li>)}
656
+ </ul>
657
+ ) : null}
658
+ </section>
659
+ );
660
+ }
661
+ `);
662
+ }
663
+ const authImport = options.features.auth
664
+ ? "import { getAuthUserId, isClerkConfigured, openAccount, subscribeToAuthState } from '@/lib/auth';\n"
665
+ : '';
548
666
  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" : '';
667
+ const itemListImport = options.apps.api && options.features.database ? "import { ItemList } from '@/components/item-list';\n" : '';
668
+ const entitlementState = options.features.billing ? " const { entitlement, error: entitlementError } = useEntitlement(clerkState === 'ready' ? clerkUserId : null);\n" : '';
550
669
  const entitlementStatus = options.features.billing ? ` {entitlement ? <p className="text-muted-foreground">Plan: {entitlement.entitlement} ({entitlement.status})</p> : null}
551
670
  {entitlementError ? <p role="alert" className="text-red-700">{entitlementError}</p> : null}
552
671
  ` : '';
553
- const authState = options.features.auth ? ` const [clerkState, setClerkState] = React.useState<'unconfigured' | 'idle' | 'loading' | 'ready' | 'error'>(
554
- () => isClerkConfigured() ? 'idle' : 'unconfigured',
672
+ const authState = options.features.auth ? ` const [clerkState, setClerkState] = React.useState<'unconfigured' | 'signed-out' | 'loading' | 'ready' | 'error'>(
673
+ () => isClerkConfigured() ? 'loading' : 'unconfigured',
555
674
  );
675
+ const [clerkUserId, setClerkUserId] = React.useState<string | null>(null);
676
+
677
+ React.useEffect(() => {
678
+ if (!isClerkConfigured()) return;
679
+ let disposed = false;
680
+ let unsubscribe: (() => void) | undefined;
681
+ void subscribeToAuthState((userId) => {
682
+ if (!disposed) {
683
+ setClerkUserId(userId);
684
+ setClerkState(userId ? 'ready' : 'signed-out');
685
+ }
686
+ }).then((stop) => {
687
+ if (disposed) stop();
688
+ else unsubscribe = stop;
689
+ }).catch(() => {
690
+ if (!disposed) setClerkState('error');
691
+ });
692
+ return () => {
693
+ disposed = true;
694
+ unsubscribe?.();
695
+ };
696
+ }, []);
556
697
 
557
698
  const handleAccount = async () => {
558
699
  setClerkState('loading');
559
700
  try {
560
701
  const opened = await openAccount();
561
- setClerkState(opened ? 'ready' : 'unconfigured');
702
+ const userId = opened ? await getAuthUserId() : null;
703
+ setClerkUserId(userId);
704
+ setClerkState(opened ? (userId ? 'ready' : 'signed-out') : 'unconfigured');
562
705
  } catch {
706
+ setClerkUserId(null);
563
707
  setClerkState('error');
564
708
  }
565
709
  };
@@ -571,19 +715,27 @@ export function Button({ className, type = 'button', ...props }: React.ButtonHTM
571
715
  className={clerkState === 'error' || clerkState === 'unconfigured' ? 'text-red-700' : 'text-muted-foreground'}
572
716
  >
573
717
  {clerkState === 'unconfigured' ? 'Set VITE_CLERK_PUBLISHABLE_KEY to enable accounts.' : null}
574
- {clerkState === 'idle' ? 'Account services load only when you open your account.' : null}
718
+ {clerkState === 'signed-out' ? 'Sign in to use account-backed features.' : null}
575
719
  {clerkState === 'loading' ? 'Loading account services…' : null}
576
- {clerkState === 'ready' ? 'Account services are ready.' : null}
720
+ {clerkState === 'ready' ? 'Signed in. Account-backed features are ready.' : null}
577
721
  {clerkState === 'error' ? 'Clerk could not initialize. Check the publishable key and network connection.' : null}
578
722
  </p>
579
723
  ` : '';
580
724
  const buttonAction = options.features.auth
581
725
  ? " disabled={clerkState === 'loading' || clerkState === 'unconfigured'} onClick={() => void handleAccount()}"
582
726
  : '';
727
+ const accountButton = options.features.auth
728
+ ? ` <Button${buttonAction}>Open account</Button>\n`
729
+ : '';
730
+ const buttonImport = options.features.auth ? "import { Button } from '@/components/ui/button';\n" : '';
731
+ const itemList = options.apps.api && options.features.database
732
+ ? options.features.auth
733
+ ? " <ItemList identity={clerkState === 'ready' ? clerkUserId : null} />\n"
734
+ : " <ItemList />\n"
735
+ : '';
583
736
  writeFile(path.join(dir, 'src/renderer/main.tsx'), `import React from 'react';
584
737
  import ReactDOM from 'react-dom/client';
585
- import { Button } from '@/components/ui/button';
586
- ${authImport}${entitlementImport}import './styles.css';
738
+ ${buttonImport}${authImport}${entitlementImport}${itemListImport}import './styles.css';
587
739
 
588
740
  function App() {
589
741
  ${authState}${entitlementState} return (
@@ -596,7 +748,8 @@ ${authState}${entitlementState} return (
596
748
  </section>
597
749
  ${authStatus}
598
750
  ${entitlementStatus}
599
- <Button${buttonAction}>${buttonLabel}</Button>
751
+ ${accountButton}
752
+ ${itemList}
600
753
  </main>
601
754
  );
602
755
  }
@@ -1,3 +1,3 @@
1
- import type { ProjectOptions } from '../scaffold.js';
1
+ import type { ProjectOptions } from '../project.js';
2
2
  export declare function desktopUpdatesBucketName(projectName: string): string;
3
3
  export declare function scaffoldElectronUpdater(root: string, options: ProjectOptions): void;
@@ -345,7 +345,7 @@ pnpm desktop:updates:cloudflare:login
345
345
  pnpm desktop:updates:first-provision
346
346
  cp apps/desktop/electron-builder.env.example apps/desktop/electron-builder.env
347
347
  pnpm desktop:updates:build:mac
348
- pnpm desktop:updates:publish -- --platform mac --arch arm64
348
+ pnpm desktop:updates:publish --platform mac --arch arm64
349
349
  \`\`\`
350
350
 
351
351
  Build each operating system on its native, signing-enabled CI runner. Increase
@@ -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>;