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.
- package/CHANGELOG.md +46 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +68 -55
- package/SKILL.md +107 -12
- package/dist/architecture/index.d.ts +2 -2
- package/dist/architecture/index.js +1 -1
- package/dist/architecture/modules.d.ts +5 -1
- package/dist/architecture/modules.js +4 -1
- package/dist/bin.js +113 -84
- package/dist/cli.d.ts +7 -7
- package/dist/cli.js +62 -16
- package/dist/dependencies.d.ts +1 -0
- package/dist/dependencies.js +11 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/project.d.ts +30 -0
- package/dist/project.js +1 -0
- package/dist/prompts.d.ts +4 -3
- package/dist/prompts.js +29 -7
- package/dist/scaffold.d.ts +12 -24
- package/dist/scaffold.js +330 -123
- package/dist/templates/api.d.ts +1 -1
- package/dist/templates/api.js +67 -2
- package/dist/templates/assets-private-proxy.d.ts +1 -1
- package/dist/templates/desktop.d.ts +1 -1
- package/dist/templates/desktop.js +189 -36
- package/dist/templates/electron-updater.d.ts +1 -1
- package/dist/templates/electron-updater.js +1 -1
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +138 -14
- package/dist/templates/first-run.d.ts +2 -0
- package/dist/templates/first-run.js +167 -0
- package/dist/templates/mobile.d.ts +1 -1
- package/dist/templates/mobile.js +158 -17
- package/dist/templates/shared.d.ts +1 -1
- package/dist/templates/shared.js +64 -7
- package/dist/templates/web.d.ts +1 -1
- package/dist/templates/web.js +185 -28
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/NORTHSTAR.md +114 -0
- package/docs/conventions.md +6 -4
- package/docs/engineering-principles.md +88 -0
- package/docs/output-tree-contract.md +16 -5
- package/docs/references/manual-scaffolding.md +22 -4
- package/docs/references/provisioning.md +434 -77
- package/package.json +3 -1
package/dist/templates/api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { ProjectOptions } from '../
|
|
1
|
+
import type { ProjectOptions } from '../project.js';
|
|
2
2
|
export declare function scaffoldApi(root: string, options: ProjectOptions): Promise<void>;
|
package/dist/templates/api.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 '../
|
|
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 '../
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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 (!
|
|
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
|
-
}, [
|
|
531
|
+
}, [identity, refresh]);
|
|
521
532
|
React.useEffect(() => {
|
|
522
|
-
if (!
|
|
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) =>
|
|
538
|
+
onError: (cause) => {
|
|
539
|
+
if (identityRef.current === identity) {
|
|
540
|
+
setLoadedIdentity(identity);
|
|
541
|
+
setError(cause.message);
|
|
542
|
+
}
|
|
543
|
+
},
|
|
528
544
|
});
|
|
529
|
-
}, [
|
|
530
|
-
return {
|
|
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
|
-
|
|
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
|
|
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' | '
|
|
554
|
-
() => isClerkConfigured() ? '
|
|
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
|
-
|
|
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 === '
|
|
718
|
+
{clerkState === 'signed-out' ? 'Sign in to use account-backed features.' : null}
|
|
575
719
|
{clerkState === 'loading' ? 'Loading account services…' : null}
|
|
576
|
-
{clerkState === 'ready' ? 'Account
|
|
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
|
-
|
|
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
|
-
|
|
751
|
+
${accountButton}
|
|
752
|
+
${itemList}
|
|
600
753
|
</main>
|
|
601
754
|
);
|
|
602
755
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ProjectOptions } from '../
|
|
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 --
|
|
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 '../
|
|
1
|
+
import type { ProjectOptions } from '../project.js';
|
|
2
2
|
export declare function scaffoldExtension(root: string, options: ProjectOptions): Promise<void>;
|