anhedral 0.4.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -265
- package/assets/anhedral-cli-init-technical.svg +322 -0
- package/assets/anhedral-cli-init.svg +257 -173
- package/dist/bin.js +28 -191
- package/dist/dependencies.d.ts +7 -57
- package/dist/dependencies.js +96 -308
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -4
- package/dist/platforms/desktop.js +401 -0
- package/dist/platforms/extension.js +401 -0
- package/dist/platforms/mobile.js +232 -0
- package/dist/project.d.ts +0 -26
- package/dist/standard-products.d.ts +2 -0
- package/dist/standard-products.js +6 -0
- package/dist/standard.d.ts +18 -0
- package/dist/standard.js +595 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/application-stack-standard.md +700 -0
- package/package.json +29 -51
- package/CHANGELOG.md +0 -178
- package/CONTRIBUTING.md +0 -77
- package/SECURITY.md +0 -37
- package/SKILL.md +0 -339
- package/assets/images/svg/ably-symbol-color.svg +0 -8
- package/bin/anhedral.js +0 -2
- package/dist/architecture/contributions.d.ts +0 -28
- package/dist/architecture/contributions.js +0 -125
- package/dist/architecture/index.d.ts +0 -12
- package/dist/architecture/index.js +0 -6
- package/dist/architecture/manifest.d.ts +0 -53
- package/dist/architecture/manifest.js +0 -283
- package/dist/architecture/modules.d.ts +0 -41
- package/dist/architecture/modules.js +0 -187
- package/dist/architecture/plan.d.ts +0 -58
- package/dist/architecture/plan.js +0 -145
- package/dist/architecture/templates.d.ts +0 -10
- package/dist/architecture/templates.js +0 -23
- package/dist/cli.d.ts +0 -32
- package/dist/cli.js +0 -281
- package/dist/composers/root.d.ts +0 -6
- package/dist/composers/root.js +0 -35
- package/dist/print.d.ts +0 -8
- package/dist/print.js +0 -48
- package/dist/prompts.d.ts +0 -6
- package/dist/prompts.js +0 -41
- package/dist/scaffold.d.ts +0 -64
- package/dist/scaffold.js +0 -2102
- package/dist/template-source.d.ts +0 -20
- package/dist/template-source.js +0 -180
- package/dist/templates/api.d.ts +0 -2
- package/dist/templates/api.js +0 -3187
- package/dist/templates/assets-private-proxy.d.ts +0 -3
- package/dist/templates/assets-private-proxy.js +0 -277
- package/dist/templates/desktop.js +0 -792
- package/dist/templates/electron-updater.d.ts +0 -3
- package/dist/templates/electron-updater.js +0 -358
- package/dist/templates/extension.js +0 -856
- package/dist/templates/first-run.d.ts +0 -2
- package/dist/templates/first-run.js +0 -167
- package/dist/templates/mobile.js +0 -861
- package/dist/templates/shared.d.ts +0 -2
- package/dist/templates/shared.js +0 -577
- package/dist/templates/web.d.ts +0 -2
- package/dist/templates/web.js +0 -569
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +0 -8
- package/dist/ui.d.ts +0 -33
- package/dist/ui.js +0 -199
- package/dist/workspace-config.d.ts +0 -11
- package/dist/workspace-config.js +0 -372
- package/docs/NORTHSTAR.md +0 -114
- package/docs/conventions.md +0 -94
- package/docs/engineering-principles.md +0 -88
- package/docs/master-stack-map.md +0 -199
- package/docs/output-tree-contract.md +0 -493
- package/docs/references/manual-scaffolding.md +0 -421
- package/docs/references/provisioning.md +0 -577
- package/scripts/render-master-stack-map.mjs +0 -510
- package/templates/api-fastify/apps/api/tsconfig.json +0 -20
- package/templates/catalog.json +0 -23
- package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
- package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
- package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
- package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
- package/templates/web-next/apps/web/next-env.d.ts +0 -6
- /package/dist/{templates → platforms}/desktop.d.ts +0 -0
- /package/dist/{templates → platforms}/extension.d.ts +0 -0
- /package/dist/{templates → platforms}/mobile.d.ts +0 -0
package/dist/templates/web.js
DELETED
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { writeFile } from '../util.js';
|
|
3
|
-
import { anhedralPrint } from '../print.js';
|
|
4
|
-
import { WEB_APP_DEPENDENCIES } from '../dependencies.js';
|
|
5
|
-
import { childPackageName, jsString } from '../render.js';
|
|
6
|
-
export async function scaffoldWeb(root, options) {
|
|
7
|
-
const dir = path.join(root, 'apps/web');
|
|
8
|
-
anhedralPrint.section('Web (Next.js + shadcn/ui)');
|
|
9
|
-
anhedralPrint.step('Materializing bundled Next.js + shadcn substrate');
|
|
10
|
-
anhedralPrint.done('Next.js + shadcn substrate materialized');
|
|
11
|
-
anhedralPrint.step('Writing web app customizations');
|
|
12
|
-
writePackageJson(dir, options);
|
|
13
|
-
writeTsConfig(dir);
|
|
14
|
-
writeNextConfig(dir);
|
|
15
|
-
writePostcssConfig(dir);
|
|
16
|
-
writeShadcnConfig(dir);
|
|
17
|
-
writeEnvExample(dir, options);
|
|
18
|
-
writeUiFiles(dir, options);
|
|
19
|
-
writeAppFiles(dir, options);
|
|
20
|
-
anhedralPrint.done('Next.js web app written');
|
|
21
|
-
}
|
|
22
|
-
function writePackageJson(dir, options) {
|
|
23
|
-
const dependencies = { ...(WEB_APP_DEPENDENCIES.dependencies ?? {}) };
|
|
24
|
-
if (!options.apps.api)
|
|
25
|
-
delete dependencies['@shared/api-client'];
|
|
26
|
-
if (!options.features.billing)
|
|
27
|
-
delete dependencies['@shared/realtime'];
|
|
28
|
-
if (!options.features.auth) {
|
|
29
|
-
delete dependencies['@clerk/nextjs'];
|
|
30
|
-
delete dependencies['@clerk/ui'];
|
|
31
|
-
}
|
|
32
|
-
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
33
|
-
name: childPackageName(options.projectName, 'web'),
|
|
34
|
-
version: '0.1.0',
|
|
35
|
-
private: true,
|
|
36
|
-
type: 'module',
|
|
37
|
-
scripts: {
|
|
38
|
-
dev: 'next dev',
|
|
39
|
-
build: 'next build',
|
|
40
|
-
start: 'next start',
|
|
41
|
-
typecheck: 'tsc --noEmit',
|
|
42
|
-
},
|
|
43
|
-
dependencies,
|
|
44
|
-
devDependencies: WEB_APP_DEPENDENCIES.devDependencies,
|
|
45
|
-
}, null, 2) + '\n');
|
|
46
|
-
}
|
|
47
|
-
function writeTsConfig(dir) {
|
|
48
|
-
writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
|
|
49
|
-
compilerOptions: {
|
|
50
|
-
target: 'ES2017',
|
|
51
|
-
lib: ['dom', 'dom.iterable', 'esnext'],
|
|
52
|
-
allowJs: true,
|
|
53
|
-
skipLibCheck: true,
|
|
54
|
-
strict: true,
|
|
55
|
-
noEmit: true,
|
|
56
|
-
esModuleInterop: true,
|
|
57
|
-
module: 'esnext',
|
|
58
|
-
moduleResolution: 'bundler',
|
|
59
|
-
resolveJsonModule: true,
|
|
60
|
-
isolatedModules: true,
|
|
61
|
-
jsx: 'react-jsx',
|
|
62
|
-
incremental: true,
|
|
63
|
-
plugins: [{ name: 'next' }],
|
|
64
|
-
paths: { '@/*': ['./*'] },
|
|
65
|
-
},
|
|
66
|
-
include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts', '.next/dev/types/**/*.ts'],
|
|
67
|
-
exclude: ['node_modules'],
|
|
68
|
-
}, null, 2) + '\n');
|
|
69
|
-
}
|
|
70
|
-
function writeNextConfig(dir) {
|
|
71
|
-
writeFile(path.join(dir, 'next.config.ts'), `import type { NextConfig } from 'next';
|
|
72
|
-
|
|
73
|
-
const nextConfig: NextConfig = {
|
|
74
|
-
async rewrites() {
|
|
75
|
-
if (process.env.NODE_ENV !== 'development') return [];
|
|
76
|
-
return [{ source: '/api/:path*', destination: 'http://localhost:8787/api/:path*' }];
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export default nextConfig;
|
|
81
|
-
`);
|
|
82
|
-
}
|
|
83
|
-
function writePostcssConfig(dir) {
|
|
84
|
-
writeFile(path.join(dir, 'postcss.config.mjs'), `const config = {
|
|
85
|
-
plugins: {
|
|
86
|
-
'@tailwindcss/postcss': {},
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export default config;
|
|
91
|
-
`);
|
|
92
|
-
}
|
|
93
|
-
function writeShadcnConfig(dir) {
|
|
94
|
-
writeFile(path.join(dir, 'components.json'), JSON.stringify({
|
|
95
|
-
$schema: 'https://ui.shadcn.com/schema.json',
|
|
96
|
-
style: 'new-york',
|
|
97
|
-
rsc: true,
|
|
98
|
-
tsx: true,
|
|
99
|
-
tailwind: {
|
|
100
|
-
config: '',
|
|
101
|
-
css: 'app/globals.css',
|
|
102
|
-
baseColor: 'neutral',
|
|
103
|
-
cssVariables: true,
|
|
104
|
-
prefix: '',
|
|
105
|
-
},
|
|
106
|
-
aliases: {
|
|
107
|
-
components: '@/components',
|
|
108
|
-
utils: '@/lib/utils',
|
|
109
|
-
ui: '@/components/ui',
|
|
110
|
-
lib: '@/lib',
|
|
111
|
-
hooks: '@/hooks',
|
|
112
|
-
},
|
|
113
|
-
iconLibrary: 'lucide',
|
|
114
|
-
}, null, 2) + '\n');
|
|
115
|
-
}
|
|
116
|
-
function writeEnvExample(dir, options) {
|
|
117
|
-
const lines = [
|
|
118
|
-
options.apps.api ? 'NEXT_PUBLIC_API_URL=http://localhost:8787/api' : null,
|
|
119
|
-
options.features.auth ? 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***' : null,
|
|
120
|
-
].filter((value) => value !== null);
|
|
121
|
-
writeFile(path.join(dir, '.env.example'), lines.join('\n') + (lines.length > 0 ? '\n' : ''));
|
|
122
|
-
}
|
|
123
|
-
function writeUiFiles(dir, options) {
|
|
124
|
-
writeFile(path.join(dir, 'lib/utils.ts'), `import { clsx, type ClassValue } from 'clsx';
|
|
125
|
-
import { twMerge } from 'tailwind-merge';
|
|
126
|
-
|
|
127
|
-
export function cn(...inputs: ClassValue[]) {
|
|
128
|
-
return twMerge(clsx(inputs));
|
|
129
|
-
}
|
|
130
|
-
`);
|
|
131
|
-
if (options.apps.api)
|
|
132
|
-
writeFile(path.join(dir, 'lib/api.ts'), `import { ApiClient, normalizeApiBaseUrl } from '@shared/api-client';
|
|
133
|
-
|
|
134
|
-
function normalizeWebApiBaseUrl(value: string): string {
|
|
135
|
-
const candidate = value.trim();
|
|
136
|
-
if (!candidate.startsWith('/')) return normalizeApiBaseUrl(candidate, 'NEXT_PUBLIC_API_URL');
|
|
137
|
-
if (candidate.startsWith('//') || candidate.includes('\\\\')) {
|
|
138
|
-
throw new Error('NEXT_PUBLIC_API_URL must be a single-slash root-relative path or an absolute URL');
|
|
139
|
-
}
|
|
140
|
-
// Client components also render during Next.js builds. The server-only origin is
|
|
141
|
-
// never requested; hydration recreates the client with the browser's real origin.
|
|
142
|
-
const origin = typeof window === 'undefined' ? 'http://localhost' : window.location.origin;
|
|
143
|
-
return normalizeApiBaseUrl(new URL(candidate, origin).toString(), 'NEXT_PUBLIC_API_URL');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function apiBaseUrl(): string {
|
|
147
|
-
const configured = process.env.NEXT_PUBLIC_API_URL?.trim();
|
|
148
|
-
const candidate = configured || (process.env.NODE_ENV === 'development' ? 'http://localhost:8787/api' : '/api');
|
|
149
|
-
const normalized = normalizeWebApiBaseUrl(candidate);
|
|
150
|
-
const url = new URL(normalized);
|
|
151
|
-
const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]';
|
|
152
|
-
if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:' && !loopback) {
|
|
153
|
-
throw new Error('NEXT_PUBLIC_API_URL must use https: in production');
|
|
154
|
-
}
|
|
155
|
-
return normalized;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function createApiClient(getToken?: () => Promise<string | null>) {
|
|
159
|
-
return new ApiClient({
|
|
160
|
-
baseUrl: apiBaseUrl(),
|
|
161
|
-
getToken: getToken ?? (async () => null),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
`);
|
|
165
|
-
if (options.apps.api) {
|
|
166
|
-
writeFile(path.join(dir, 'hooks/use-api-client.ts'), options.features.auth ? `'use client';
|
|
167
|
-
|
|
168
|
-
import { useAuth } from '@clerk/nextjs';
|
|
169
|
-
import { useMemo } from 'react';
|
|
170
|
-
import { createApiClient } from '@/lib/api';
|
|
171
|
-
|
|
172
|
-
export function useApiClient() {
|
|
173
|
-
const { getToken } = useAuth();
|
|
174
|
-
return useMemo(() => createApiClient(() => getToken()), [getToken]);
|
|
175
|
-
}
|
|
176
|
-
` : `'use client';
|
|
177
|
-
|
|
178
|
-
import { useMemo } from 'react';
|
|
179
|
-
import { createApiClient } from '@/lib/api';
|
|
180
|
-
|
|
181
|
-
export function useApiClient() {
|
|
182
|
-
return useMemo(() => createApiClient(), []);
|
|
183
|
-
}
|
|
184
|
-
`);
|
|
185
|
-
}
|
|
186
|
-
if (options.apps.api && options.features.database) {
|
|
187
|
-
const authImport = options.features.auth ? "import { useAuth } from '@clerk/nextjs';\n" : '';
|
|
188
|
-
const authState = options.features.auth ? ' const { isLoaded, isSignedIn, userId } = useAuth();\n' : '';
|
|
189
|
-
const identityState = options.features.auth
|
|
190
|
-
? ' const identity = isLoaded && isSignedIn ? userId ?? null : null;\n'
|
|
191
|
-
: " const identity = 'public';\n";
|
|
192
|
-
const authGuard = options.features.auth
|
|
193
|
-
? ` if (!isLoaded || (isSignedIn && !userId)) return <p className="text-sm text-muted-foreground">Loading account…</p>;
|
|
194
|
-
if (!isSignedIn) return <p className="text-sm text-muted-foreground">Sign in to use the working starter feature.</p>;
|
|
195
|
-
|
|
196
|
-
`
|
|
197
|
-
: '';
|
|
198
|
-
writeFile(path.join(dir, 'components/item-list.tsx'), `'use client';
|
|
199
|
-
|
|
200
|
-
import { createItem, listItems, type Item } from '@shared/api-client';
|
|
201
|
-
${authImport}import * as React from 'react';
|
|
202
|
-
import { useApiClient } from '@/hooks/use-api-client';
|
|
203
|
-
import { Button } from '@/components/ui/button';
|
|
204
|
-
|
|
205
|
-
export function ItemList() {
|
|
206
|
-
${authState}${identityState} const api = useApiClient();
|
|
207
|
-
const [items, setItems] = React.useState<Item[]>([]);
|
|
208
|
-
const [loadedIdentity, setLoadedIdentity] = React.useState<string | null>(null);
|
|
209
|
-
const [name, setName] = React.useState('');
|
|
210
|
-
const [status, setStatus] = React.useState<'loading' | 'ready' | 'saving'>('loading');
|
|
211
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
212
|
-
const identityRef = React.useRef(identity);
|
|
213
|
-
const visibleItems = loadedIdentity === identity ? items : [];
|
|
214
|
-
const visibleError = loadedIdentity === identity ? error : null;
|
|
215
|
-
const isLoading = status === 'loading' || loadedIdentity !== identity;
|
|
216
|
-
|
|
217
|
-
React.useEffect(() => {
|
|
218
|
-
identityRef.current = identity;
|
|
219
|
-
if (!identity) return;
|
|
220
|
-
let active = true;
|
|
221
|
-
setLoadedIdentity(identity);
|
|
222
|
-
setItems([]);
|
|
223
|
-
setName('');
|
|
224
|
-
setError(null);
|
|
225
|
-
setStatus('loading');
|
|
226
|
-
void listItems(api).then((nextItems) => {
|
|
227
|
-
if (active) setItems(nextItems);
|
|
228
|
-
}).catch((cause: unknown) => {
|
|
229
|
-
if (active) setError(cause instanceof Error ? cause.message : 'Unable to load items');
|
|
230
|
-
}).finally(() => {
|
|
231
|
-
if (active) setStatus('ready');
|
|
232
|
-
});
|
|
233
|
-
return () => { active = false; };
|
|
234
|
-
}, [api, identity]);
|
|
235
|
-
|
|
236
|
-
async function submit(event: React.FormEvent<HTMLFormElement>) {
|
|
237
|
-
event.preventDefault();
|
|
238
|
-
const nextName = name.trim();
|
|
239
|
-
if (!identity || !nextName || status !== 'ready' || loadedIdentity !== identity) return;
|
|
240
|
-
const submittedIdentity = identity;
|
|
241
|
-
setStatus('saving');
|
|
242
|
-
try {
|
|
243
|
-
const created = await createItem(api, { name: nextName });
|
|
244
|
-
if (identityRef.current !== submittedIdentity) return;
|
|
245
|
-
setItems((current) => [created, ...current]);
|
|
246
|
-
setName('');
|
|
247
|
-
setError(null);
|
|
248
|
-
} catch (cause) {
|
|
249
|
-
if (identityRef.current === submittedIdentity) {
|
|
250
|
-
setError(cause instanceof Error ? cause.message : 'Unable to create item');
|
|
251
|
-
}
|
|
252
|
-
} finally {
|
|
253
|
-
if (identityRef.current === submittedIdentity) setStatus('ready');
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
${authGuard} return (
|
|
258
|
-
<div className="space-y-4">
|
|
259
|
-
<form className="flex gap-2" onSubmit={(event) => void submit(event)}>
|
|
260
|
-
<label className="sr-only" htmlFor="item-name">Item name</label>
|
|
261
|
-
<input
|
|
262
|
-
id="item-name"
|
|
263
|
-
className="h-9 flex-1 rounded-md border border-input bg-background px-3 text-sm"
|
|
264
|
-
maxLength={120}
|
|
265
|
-
onChange={(event) => setName(event.target.value)}
|
|
266
|
-
placeholder="Your first item"
|
|
267
|
-
value={name}
|
|
268
|
-
/>
|
|
269
|
-
<Button disabled={status !== 'ready' || loadedIdentity !== identity || !name.trim()} type="submit">
|
|
270
|
-
{status === 'saving' ? 'Adding…' : 'Add item'}
|
|
271
|
-
</Button>
|
|
272
|
-
</form>
|
|
273
|
-
{visibleError ? (
|
|
274
|
-
<p className="text-sm text-red-700" role="alert">
|
|
275
|
-
{visibleError}. Check DATABASE_URL, run pnpm db:migrate, and make sure the API is running.
|
|
276
|
-
</p>
|
|
277
|
-
) : null}
|
|
278
|
-
{isLoading ? <p className="text-sm text-muted-foreground">Loading items…</p> : null}
|
|
279
|
-
{!isLoading && visibleItems.length === 0 && !visibleError
|
|
280
|
-
? <p className="text-sm text-muted-foreground">Your database is connected. Add the first item.</p>
|
|
281
|
-
: null}
|
|
282
|
-
{visibleItems.length > 0 ? (
|
|
283
|
-
<ul className="divide-y rounded-md border">
|
|
284
|
-
{visibleItems.map((item) => <li className="px-3 py-2 text-sm" key={item.id}>{item.name}</li>)}
|
|
285
|
-
</ul>
|
|
286
|
-
) : null}
|
|
287
|
-
</div>
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
`);
|
|
291
|
-
}
|
|
292
|
-
if (options.features.billing) {
|
|
293
|
-
writeFile(path.join(dir, 'hooks/use-entitlement.ts'), `'use client';
|
|
294
|
-
|
|
295
|
-
import { subscribeToSubscriptionChanges } from '@shared/realtime';
|
|
296
|
-
import { useAuth } from '@clerk/nextjs';
|
|
297
|
-
import * as React from 'react';
|
|
298
|
-
import { useApiClient } from './use-api-client';
|
|
299
|
-
|
|
300
|
-
export function useEntitlement() {
|
|
301
|
-
const api = useApiClient();
|
|
302
|
-
const { isLoaded, isSignedIn, userId } = useAuth();
|
|
303
|
-
const identity = isLoaded && isSignedIn ? userId : null;
|
|
304
|
-
const [state, setState] = React.useState<Awaited<ReturnType<typeof api.getEntitlement>> | null>(null);
|
|
305
|
-
const [loadedIdentity, setLoadedIdentity] = React.useState<string | null>(null);
|
|
306
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
307
|
-
const revision = React.useRef(0);
|
|
308
|
-
const identityRef = React.useRef(identity);
|
|
309
|
-
const refresh = React.useCallback(async () => {
|
|
310
|
-
if (!identity) return;
|
|
311
|
-
try {
|
|
312
|
-
const next = await api.getEntitlement();
|
|
313
|
-
if (identityRef.current !== identity) return;
|
|
314
|
-
revision.current = Math.max(revision.current, next.revision);
|
|
315
|
-
setState(next);
|
|
316
|
-
setLoadedIdentity(identity);
|
|
317
|
-
setError(null);
|
|
318
|
-
} catch (cause) {
|
|
319
|
-
if (identityRef.current !== identity) return;
|
|
320
|
-
setLoadedIdentity(identity);
|
|
321
|
-
setError(cause instanceof Error ? cause.message : 'Unable to load subscription');
|
|
322
|
-
}
|
|
323
|
-
}, [api, identity]);
|
|
324
|
-
|
|
325
|
-
React.useEffect(() => {
|
|
326
|
-
identityRef.current = identity;
|
|
327
|
-
revision.current = 0;
|
|
328
|
-
setState(null);
|
|
329
|
-
setLoadedIdentity(null);
|
|
330
|
-
setError(null);
|
|
331
|
-
void refresh();
|
|
332
|
-
}, [identity, refresh]);
|
|
333
|
-
React.useEffect(() => {
|
|
334
|
-
const onVisibility = () => { if (document.visibilityState === 'visible') void refresh(); };
|
|
335
|
-
document.addEventListener('visibilitychange', onVisibility);
|
|
336
|
-
return () => document.removeEventListener('visibilitychange', onVisibility);
|
|
337
|
-
}, [refresh]);
|
|
338
|
-
React.useEffect(() => {
|
|
339
|
-
if (!identity) return;
|
|
340
|
-
return subscribeToSubscriptionChanges({
|
|
341
|
-
userId: identity,
|
|
342
|
-
getTokenRequest: () => api.getRealtimeToken(),
|
|
343
|
-
onChange: (nextRevision) => {
|
|
344
|
-
if (nextRevision > revision.current) void refresh();
|
|
345
|
-
},
|
|
346
|
-
onError: (cause) => {
|
|
347
|
-
if (identityRef.current === identity) {
|
|
348
|
-
setLoadedIdentity(identity);
|
|
349
|
-
setError(cause.message);
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
}, [api, identity, refresh]);
|
|
354
|
-
return {
|
|
355
|
-
entitlement: loadedIdentity === identity ? state : null,
|
|
356
|
-
error: loadedIdentity === identity ? error : null,
|
|
357
|
-
refresh,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
`);
|
|
361
|
-
writeFile(path.join(dir, 'components/subscription-status.tsx'), `'use client';
|
|
362
|
-
|
|
363
|
-
import { useEntitlement } from '@/hooks/use-entitlement';
|
|
364
|
-
|
|
365
|
-
export function SubscriptionStatus() {
|
|
366
|
-
const { entitlement, error } = useEntitlement();
|
|
367
|
-
if (error) return <p role="alert" className="text-sm text-red-700">{error}</p>;
|
|
368
|
-
if (!entitlement) return <p className="text-sm text-muted-foreground">Loading subscription…</p>;
|
|
369
|
-
return <p className="text-sm text-muted-foreground">Plan: {entitlement.entitlement} ({entitlement.status})</p>;
|
|
370
|
-
}
|
|
371
|
-
`);
|
|
372
|
-
}
|
|
373
|
-
if (options.features.auth) {
|
|
374
|
-
writeFile(path.join(dir, 'components/account-actions.tsx'), `'use client';
|
|
375
|
-
|
|
376
|
-
import { Show, SignInButton, UserButton } from '@clerk/nextjs';
|
|
377
|
-
import { Button } from '@/components/ui/button';
|
|
378
|
-
|
|
379
|
-
export function AccountActions() {
|
|
380
|
-
return (
|
|
381
|
-
<>
|
|
382
|
-
<Show when="signed-out">
|
|
383
|
-
<SignInButton mode="modal">
|
|
384
|
-
<Button variant="outline">Sign in</Button>
|
|
385
|
-
</SignInButton>
|
|
386
|
-
</Show>
|
|
387
|
-
<Show when="signed-in">
|
|
388
|
-
<UserButton />
|
|
389
|
-
</Show>
|
|
390
|
-
</>
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
`);
|
|
394
|
-
}
|
|
395
|
-
writeFile(path.join(dir, 'components/ui/button.tsx'), `import * as React from 'react';
|
|
396
|
-
import { cn } from '@/lib/utils';
|
|
397
|
-
|
|
398
|
-
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
399
|
-
variant?: 'default' | 'outline';
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
export function Button({ className, variant = 'default', type = 'button', ...props }: ButtonProps) {
|
|
403
|
-
return (
|
|
404
|
-
<button
|
|
405
|
-
type={type}
|
|
406
|
-
className={cn(
|
|
407
|
-
'inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50',
|
|
408
|
-
variant === 'outline' ? 'border border-input bg-background hover:bg-accent' : 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
409
|
-
className,
|
|
410
|
-
)}
|
|
411
|
-
{...props}
|
|
412
|
-
/>
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
`);
|
|
416
|
-
writeFile(path.join(dir, 'components/ui/card.tsx'), `import * as React from 'react';
|
|
417
|
-
import { cn } from '@/lib/utils';
|
|
418
|
-
|
|
419
|
-
export function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|
420
|
-
return <div className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)} {...props} />;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
424
|
-
return <div className={cn('flex flex-col gap-1.5 p-6', className)} {...props} />;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export function CardTitle({ className, ...props }: React.ComponentProps<'h3'>) {
|
|
428
|
-
return <h3 className={cn('text-2xl font-semibold leading-none', className)} {...props} />;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
432
|
-
return <div className={cn('p-6 pt-0', className)} {...props} />;
|
|
433
|
-
}
|
|
434
|
-
`);
|
|
435
|
-
}
|
|
436
|
-
function writeAppFiles(dir, options) {
|
|
437
|
-
const { displayName } = options;
|
|
438
|
-
const displayNameLiteral = jsString(displayName);
|
|
439
|
-
const descriptionLiteral = jsString(`${displayName} web app`);
|
|
440
|
-
const accountActionsImport = options.features.auth
|
|
441
|
-
? "import { AccountActions } from '@/components/account-actions';\n"
|
|
442
|
-
: '';
|
|
443
|
-
const accountAction = options.features.auth
|
|
444
|
-
? `{process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY && !process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.includes('***') ? (
|
|
445
|
-
<AccountActions />
|
|
446
|
-
) : (
|
|
447
|
-
<Button variant="outline" disabled>Configure Clerk to sign in</Button>
|
|
448
|
-
)}`
|
|
449
|
-
: '';
|
|
450
|
-
const starterLink = options.apps.api && options.features.database
|
|
451
|
-
? '<a className="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" href="#starter-feature">Open starter feature</a>'
|
|
452
|
-
: '';
|
|
453
|
-
const heroActions = starterLink || accountAction
|
|
454
|
-
? ` <div className="flex gap-3">
|
|
455
|
-
${starterLink}
|
|
456
|
-
${accountAction}
|
|
457
|
-
</div>
|
|
458
|
-
`
|
|
459
|
-
: '';
|
|
460
|
-
const clerkStyles = options.features.auth ? '@import "@clerk/ui/themes/shadcn.css";\n' : '';
|
|
461
|
-
writeFile(path.join(dir, 'app/globals.css'), `@import "tailwindcss";
|
|
462
|
-
${clerkStyles}
|
|
463
|
-
|
|
464
|
-
:root {
|
|
465
|
-
--background: oklch(1 0 0);
|
|
466
|
-
--foreground: oklch(0.145 0 0);
|
|
467
|
-
--card: oklch(1 0 0);
|
|
468
|
-
--card-foreground: oklch(0.145 0 0);
|
|
469
|
-
--primary: oklch(0.205 0 0);
|
|
470
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
471
|
-
--muted: oklch(0.97 0 0);
|
|
472
|
-
--muted-foreground: oklch(0.556 0 0);
|
|
473
|
-
--accent: oklch(0.97 0 0);
|
|
474
|
-
--border: oklch(0.922 0 0);
|
|
475
|
-
--input: oklch(0.922 0 0);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
@theme inline {
|
|
479
|
-
--color-background: var(--background);
|
|
480
|
-
--color-foreground: var(--foreground);
|
|
481
|
-
--color-card: var(--card);
|
|
482
|
-
--color-card-foreground: var(--card-foreground);
|
|
483
|
-
--color-primary: var(--primary);
|
|
484
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
485
|
-
--color-muted: var(--muted);
|
|
486
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
487
|
-
--color-accent: var(--accent);
|
|
488
|
-
--color-border: var(--border);
|
|
489
|
-
--color-input: var(--input);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
body {
|
|
493
|
-
background: var(--background);
|
|
494
|
-
color: var(--foreground);
|
|
495
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
496
|
-
}
|
|
497
|
-
`);
|
|
498
|
-
const authImport = options.features.auth
|
|
499
|
-
? "import { ClerkProvider } from '@clerk/nextjs';\nimport { shadcn } from '@clerk/ui/themes';\n"
|
|
500
|
-
: '';
|
|
501
|
-
const providers = options.features.auth ? `function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
502
|
-
const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
|
503
|
-
if (!publishableKey || publishableKey.includes('***')) return <>{children}</>;
|
|
504
|
-
return <ClerkProvider publishableKey={publishableKey} appearance={{ theme: shadcn }}>{children}</ClerkProvider>;
|
|
505
|
-
}` : `function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
506
|
-
return <>{children}</>;
|
|
507
|
-
}`;
|
|
508
|
-
writeFile(path.join(dir, 'app/layout.tsx'), `import type { Metadata } from 'next';
|
|
509
|
-
${authImport}import './globals.css';
|
|
510
|
-
|
|
511
|
-
export const metadata: Metadata = {
|
|
512
|
-
title: ${displayNameLiteral},
|
|
513
|
-
description: ${descriptionLiteral},
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
${providers}
|
|
517
|
-
|
|
518
|
-
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
519
|
-
return (
|
|
520
|
-
<html lang="en">
|
|
521
|
-
<body>
|
|
522
|
-
<Providers>{children}</Providers>
|
|
523
|
-
</body>
|
|
524
|
-
</html>
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
`);
|
|
528
|
-
const subscriptionImport = options.features.billing ? "import { SubscriptionStatus } from '@/components/subscription-status';\n" : '';
|
|
529
|
-
const itemListImport = options.apps.api && options.features.database ? "import { ItemList } from '@/components/item-list';\n" : '';
|
|
530
|
-
const subscriptionStatus = options.features.billing
|
|
531
|
-
? " {process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY && !process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.includes('***') ? <SubscriptionStatus /> : null}\n"
|
|
532
|
-
: '';
|
|
533
|
-
const starterContent = options.apps.api && options.features.database
|
|
534
|
-
? options.features.auth
|
|
535
|
-
? `{process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY && !process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.includes('***')
|
|
536
|
-
? <ItemList />
|
|
537
|
-
: <p>Configure Clerk to activate the starter feature.</p>}`
|
|
538
|
-
: '<ItemList />'
|
|
539
|
-
: options.apps.api
|
|
540
|
-
? 'Contracts, API access, and configuration are shared through workspace packages.'
|
|
541
|
-
: 'Add modules later with pnpm anhedral:add; user-owned source remains untouched.';
|
|
542
|
-
const starterTitle = options.apps.api && options.features.database ? 'Working starter feature' : 'Shared modules';
|
|
543
|
-
const buttonImport = options.features.auth ? "import { Button } from '@/components/ui/button';\n" : '';
|
|
544
|
-
writeFile(path.join(dir, 'app/page.tsx'), `${accountActionsImport}${subscriptionImport}${itemListImport}import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
545
|
-
${buttonImport}
|
|
546
|
-
|
|
547
|
-
export default function Home() {
|
|
548
|
-
return (
|
|
549
|
-
<main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-8 px-6 py-10">
|
|
550
|
-
<section className="flex flex-col gap-3">
|
|
551
|
-
<h1 className="text-4xl font-semibold tracking-normal">{${displayNameLiteral}}</h1>
|
|
552
|
-
<p className="max-w-2xl text-muted-foreground">
|
|
553
|
-
${options.apps.api ? 'Next.js + shadcn/ui web client connected to the shared Fastify API.' : 'Next.js + shadcn/ui web application.'}
|
|
554
|
-
</p>
|
|
555
|
-
${heroActions}
|
|
556
|
-
${subscriptionStatus} </section>
|
|
557
|
-
<Card id="starter-feature">
|
|
558
|
-
<CardHeader>
|
|
559
|
-
<CardTitle>${starterTitle}</CardTitle>
|
|
560
|
-
</CardHeader>
|
|
561
|
-
<CardContent className="text-sm text-muted-foreground">
|
|
562
|
-
${starterContent}
|
|
563
|
-
</CardContent>
|
|
564
|
-
</Card>
|
|
565
|
-
</main>
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
`);
|
|
569
|
-
}
|
package/dist/toolchain.d.ts
DELETED
package/dist/toolchain.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export const TOOLCHAIN_CHANNELS = ['latest', 'stable'];
|
|
2
|
-
export function resolveToolchainChannel(value) {
|
|
3
|
-
if (!value)
|
|
4
|
-
return 'stable';
|
|
5
|
-
if (value === 'latest' || value === 'stable')
|
|
6
|
-
return value;
|
|
7
|
-
throw new Error(`--toolchain must be one of: ${TOOLCHAIN_CHANNELS.join(', ')}`);
|
|
8
|
-
}
|
package/dist/ui.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export declare const UI_TARGETS: readonly ["web", "mobile", "desktop", "extension"];
|
|
2
|
-
export declare const NATIVE_STYLING_LIBRARIES: readonly ["nativewind", "uniwind"];
|
|
3
|
-
export declare const UI_PROVIDERS: readonly ["shadcn", "react-native-reusables"];
|
|
4
|
-
export type UiTarget = (typeof UI_TARGETS)[number];
|
|
5
|
-
export type NativeStylingLibrary = (typeof NATIVE_STYLING_LIBRARIES)[number];
|
|
6
|
-
export type UiProvider = (typeof UI_PROVIDERS)[number];
|
|
7
|
-
export type UiComponentInstall = {
|
|
8
|
-
readonly name: string;
|
|
9
|
-
readonly target: UiTarget;
|
|
10
|
-
readonly provider: UiProvider;
|
|
11
|
-
readonly source: string;
|
|
12
|
-
readonly variant: NativeStylingLibrary | null;
|
|
13
|
-
};
|
|
14
|
-
export type UiInstallCommand = {
|
|
15
|
-
readonly target: UiTarget;
|
|
16
|
-
readonly cwd: string;
|
|
17
|
-
readonly executable: 'pnpm';
|
|
18
|
-
readonly args: readonly string[];
|
|
19
|
-
readonly installs: readonly UiComponentInstall[];
|
|
20
|
-
};
|
|
21
|
-
export declare function reconcileUiWorkspaceDependencies(workspaceRoot: string, targetDirectory: string): void;
|
|
22
|
-
export declare function isUiTarget(value: unknown): value is UiTarget;
|
|
23
|
-
export declare function isNativeStylingLibrary(value: unknown): value is NativeStylingLibrary;
|
|
24
|
-
export declare function isUiProvider(value: unknown): value is UiProvider;
|
|
25
|
-
export declare function normalizeUiComponentName(value: string): string;
|
|
26
|
-
export declare function parseUiComponentList(value: string): string[];
|
|
27
|
-
export declare function providerForTarget(target: UiTarget): UiProvider;
|
|
28
|
-
export declare function registrySourceFor(name: string, target: UiTarget, nativeStyling: NativeStylingLibrary): string;
|
|
29
|
-
export declare function resolveUiInstalls(componentNames: readonly string[], targets: readonly UiTarget[], nativeStyling: NativeStylingLibrary): readonly UiComponentInstall[];
|
|
30
|
-
export declare function uiInstallKey(install: Pick<UiComponentInstall, 'target' | 'name'>): string;
|
|
31
|
-
export declare function mergeUiInstalls(current: readonly UiComponentInstall[], additions: readonly UiComponentInstall[]): readonly UiComponentInstall[];
|
|
32
|
-
export declare function buildUiInstallCommands(root: string, installs: readonly UiComponentInstall[]): readonly UiInstallCommand[];
|
|
33
|
-
export declare function installUiComponents(root: string, installs: readonly UiComponentInstall[]): void;
|