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.
- package/README.md +29 -256
- package/assets/anhedral-cli-init-technical.svg +322 -0
- package/assets/anhedral-cli-init.svg +257 -173
- package/dist/bin.js +28 -162
- package/dist/dependencies.d.ts +7 -56
- package/dist/dependencies.js +96 -305
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -4
- package/dist/{templates → platforms}/desktop.d.ts +1 -1
- package/dist/{templates → platforms}/desktop.js +28 -266
- package/dist/{templates → platforms}/extension.d.ts +1 -1
- package/dist/platforms/extension.js +401 -0
- package/dist/{templates → platforms}/mobile.d.ts +1 -1
- package/dist/platforms/mobile.js +232 -0
- package/dist/project.d.ts +4 -0
- package/dist/project.js +1 -0
- 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 +578 -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 +34 -49
- package/CHANGELOG.md +0 -132
- package/CONTRIBUTING.md +0 -73
- package/SECURITY.md +0 -37
- package/SKILL.md +0 -244
- 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 -37
- package/dist/architecture/modules.js +0 -184
- 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 -235
- 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 -5
- package/dist/prompts.js +0 -19
- package/dist/scaffold.d.ts +0 -76
- package/dist/scaffold.js +0 -1895
- 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 -3122
- package/dist/templates/assets-private-proxy.d.ts +0 -3
- package/dist/templates/assets-private-proxy.js +0 -277
- package/dist/templates/electron-updater.d.ts +0 -3
- package/dist/templates/electron-updater.js +0 -358
- package/dist/templates/extension.js +0 -732
- package/dist/templates/mobile.js +0 -720
- package/dist/templates/shared.d.ts +0 -2
- package/dist/templates/shared.js +0 -520
- package/dist/templates/web.d.ts +0 -2
- package/dist/templates/web.js +0 -412
- 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/conventions.md +0 -92
- package/docs/master-stack-map.md +0 -199
- package/docs/output-tree-contract.md +0 -482
- package/docs/references/manual-scaffolding.md +0 -403
- package/docs/references/provisioning.md +0 -220
- 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/web.js
DELETED
|
@@ -1,412 +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
|
-
if (typeof window === 'undefined') {
|
|
141
|
-
throw new Error('A root-relative NEXT_PUBLIC_API_URL can only be resolved in the browser');
|
|
142
|
-
}
|
|
143
|
-
return normalizeApiBaseUrl(new URL(candidate, window.location.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
|
-
if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:') {
|
|
152
|
-
throw new Error('NEXT_PUBLIC_API_URL must use https: in production');
|
|
153
|
-
}
|
|
154
|
-
return normalized;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function createApiClient(getToken?: () => Promise<string | null>) {
|
|
158
|
-
return new ApiClient({
|
|
159
|
-
baseUrl: apiBaseUrl(),
|
|
160
|
-
getToken: getToken ?? (async () => null),
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
`);
|
|
164
|
-
if (options.apps.api && options.features.auth) {
|
|
165
|
-
writeFile(path.join(dir, 'hooks/use-api-client.ts'), `'use client';
|
|
166
|
-
|
|
167
|
-
import { useAuth } from '@clerk/nextjs';
|
|
168
|
-
import { useMemo } from 'react';
|
|
169
|
-
import { createApiClient } from '@/lib/api';
|
|
170
|
-
|
|
171
|
-
export function useApiClient() {
|
|
172
|
-
const { getToken } = useAuth();
|
|
173
|
-
return useMemo(() => createApiClient(() => getToken()), [getToken]);
|
|
174
|
-
}
|
|
175
|
-
`);
|
|
176
|
-
}
|
|
177
|
-
if (options.features.billing) {
|
|
178
|
-
writeFile(path.join(dir, 'hooks/use-entitlement.ts'), `'use client';
|
|
179
|
-
|
|
180
|
-
import { subscribeToSubscriptionChanges } from '@shared/realtime';
|
|
181
|
-
import { useAuth } from '@clerk/nextjs';
|
|
182
|
-
import * as React from 'react';
|
|
183
|
-
import { useApiClient } from './use-api-client';
|
|
184
|
-
|
|
185
|
-
export function useEntitlement() {
|
|
186
|
-
const api = useApiClient();
|
|
187
|
-
const { isLoaded, isSignedIn, userId } = useAuth();
|
|
188
|
-
const [state, setState] = React.useState<Awaited<ReturnType<typeof api.getEntitlement>> | null>(null);
|
|
189
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
190
|
-
const revision = React.useRef(0);
|
|
191
|
-
const refresh = React.useCallback(async () => {
|
|
192
|
-
if (!isLoaded || !isSignedIn) return;
|
|
193
|
-
try {
|
|
194
|
-
const next = await api.getEntitlement();
|
|
195
|
-
revision.current = Math.max(revision.current, next.revision);
|
|
196
|
-
setState(next);
|
|
197
|
-
setError(null);
|
|
198
|
-
} catch (cause) {
|
|
199
|
-
setError(cause instanceof Error ? cause.message : 'Unable to load subscription');
|
|
200
|
-
}
|
|
201
|
-
}, [api, isLoaded, isSignedIn]);
|
|
202
|
-
|
|
203
|
-
React.useEffect(() => { void refresh(); }, [refresh]);
|
|
204
|
-
React.useEffect(() => {
|
|
205
|
-
const onVisibility = () => { if (document.visibilityState === 'visible') void refresh(); };
|
|
206
|
-
document.addEventListener('visibilitychange', onVisibility);
|
|
207
|
-
return () => document.removeEventListener('visibilitychange', onVisibility);
|
|
208
|
-
}, [refresh]);
|
|
209
|
-
React.useEffect(() => {
|
|
210
|
-
if (!userId || !isSignedIn) return;
|
|
211
|
-
return subscribeToSubscriptionChanges({
|
|
212
|
-
userId,
|
|
213
|
-
getTokenRequest: () => api.getRealtimeToken(),
|
|
214
|
-
onChange: (nextRevision) => {
|
|
215
|
-
if (nextRevision > revision.current) void refresh();
|
|
216
|
-
},
|
|
217
|
-
onError: (cause) => setError(cause.message),
|
|
218
|
-
});
|
|
219
|
-
}, [api, isSignedIn, refresh, userId]);
|
|
220
|
-
return { entitlement: state, error, refresh };
|
|
221
|
-
}
|
|
222
|
-
`);
|
|
223
|
-
writeFile(path.join(dir, 'components/subscription-status.tsx'), `'use client';
|
|
224
|
-
|
|
225
|
-
import { useEntitlement } from '@/hooks/use-entitlement';
|
|
226
|
-
|
|
227
|
-
export function SubscriptionStatus() {
|
|
228
|
-
const { entitlement, error } = useEntitlement();
|
|
229
|
-
if (error) return <p role="alert" className="text-sm text-red-700">{error}</p>;
|
|
230
|
-
if (!entitlement) return <p className="text-sm text-muted-foreground">Loading subscription…</p>;
|
|
231
|
-
return <p className="text-sm text-muted-foreground">Plan: {entitlement.entitlement} ({entitlement.status})</p>;
|
|
232
|
-
}
|
|
233
|
-
`);
|
|
234
|
-
}
|
|
235
|
-
if (options.features.auth) {
|
|
236
|
-
writeFile(path.join(dir, 'components/account-actions.tsx'), `'use client';
|
|
237
|
-
|
|
238
|
-
import { Show, SignInButton, UserButton } from '@clerk/nextjs';
|
|
239
|
-
import { Button } from '@/components/ui/button';
|
|
240
|
-
|
|
241
|
-
export function AccountActions() {
|
|
242
|
-
return (
|
|
243
|
-
<>
|
|
244
|
-
<Show when="signed-out">
|
|
245
|
-
<SignInButton mode="modal">
|
|
246
|
-
<Button variant="outline">Sign in</Button>
|
|
247
|
-
</SignInButton>
|
|
248
|
-
</Show>
|
|
249
|
-
<Show when="signed-in">
|
|
250
|
-
<UserButton />
|
|
251
|
-
</Show>
|
|
252
|
-
</>
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
`);
|
|
256
|
-
}
|
|
257
|
-
writeFile(path.join(dir, 'components/ui/button.tsx'), `import * as React from 'react';
|
|
258
|
-
import { cn } from '@/lib/utils';
|
|
259
|
-
|
|
260
|
-
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
261
|
-
variant?: 'default' | 'outline';
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
export function Button({ className, variant = 'default', type = 'button', ...props }: ButtonProps) {
|
|
265
|
-
return (
|
|
266
|
-
<button
|
|
267
|
-
type={type}
|
|
268
|
-
className={cn(
|
|
269
|
-
'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',
|
|
270
|
-
variant === 'outline' ? 'border border-input bg-background hover:bg-accent' : 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
271
|
-
className,
|
|
272
|
-
)}
|
|
273
|
-
{...props}
|
|
274
|
-
/>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
`);
|
|
278
|
-
writeFile(path.join(dir, 'components/ui/card.tsx'), `import * as React from 'react';
|
|
279
|
-
import { cn } from '@/lib/utils';
|
|
280
|
-
|
|
281
|
-
export function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|
282
|
-
return <div className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)} {...props} />;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
286
|
-
return <div className={cn('flex flex-col gap-1.5 p-6', className)} {...props} />;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function CardTitle({ className, ...props }: React.ComponentProps<'h3'>) {
|
|
290
|
-
return <h3 className={cn('text-2xl font-semibold leading-none', className)} {...props} />;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
294
|
-
return <div className={cn('p-6 pt-0', className)} {...props} />;
|
|
295
|
-
}
|
|
296
|
-
`);
|
|
297
|
-
}
|
|
298
|
-
function writeAppFiles(dir, options) {
|
|
299
|
-
const { displayName } = options;
|
|
300
|
-
const displayNameLiteral = jsString(displayName);
|
|
301
|
-
const descriptionLiteral = jsString(`${displayName} web app`);
|
|
302
|
-
const accountActionsImport = options.features.auth
|
|
303
|
-
? "import { AccountActions } from '@/components/account-actions';\n"
|
|
304
|
-
: '';
|
|
305
|
-
const accountAction = options.features.auth
|
|
306
|
-
? `{process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ? (
|
|
307
|
-
<AccountActions />
|
|
308
|
-
) : (
|
|
309
|
-
<Button variant="outline" disabled>Configure Clerk to sign in</Button>
|
|
310
|
-
)}`
|
|
311
|
-
: '<Button variant="outline">View account</Button>';
|
|
312
|
-
const clerkStyles = options.features.auth ? '@import "@clerk/ui/themes/shadcn.css";\n' : '';
|
|
313
|
-
writeFile(path.join(dir, 'app/globals.css'), `@import "tailwindcss";
|
|
314
|
-
${clerkStyles}
|
|
315
|
-
|
|
316
|
-
:root {
|
|
317
|
-
--background: oklch(1 0 0);
|
|
318
|
-
--foreground: oklch(0.145 0 0);
|
|
319
|
-
--card: oklch(1 0 0);
|
|
320
|
-
--card-foreground: oklch(0.145 0 0);
|
|
321
|
-
--primary: oklch(0.205 0 0);
|
|
322
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
323
|
-
--muted: oklch(0.97 0 0);
|
|
324
|
-
--muted-foreground: oklch(0.556 0 0);
|
|
325
|
-
--accent: oklch(0.97 0 0);
|
|
326
|
-
--border: oklch(0.922 0 0);
|
|
327
|
-
--input: oklch(0.922 0 0);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
@theme inline {
|
|
331
|
-
--color-background: var(--background);
|
|
332
|
-
--color-foreground: var(--foreground);
|
|
333
|
-
--color-card: var(--card);
|
|
334
|
-
--color-card-foreground: var(--card-foreground);
|
|
335
|
-
--color-primary: var(--primary);
|
|
336
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
337
|
-
--color-muted: var(--muted);
|
|
338
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
339
|
-
--color-accent: var(--accent);
|
|
340
|
-
--color-border: var(--border);
|
|
341
|
-
--color-input: var(--input);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
body {
|
|
345
|
-
background: var(--background);
|
|
346
|
-
color: var(--foreground);
|
|
347
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
348
|
-
}
|
|
349
|
-
`);
|
|
350
|
-
const authImport = options.features.auth
|
|
351
|
-
? "import { ClerkProvider } from '@clerk/nextjs';\nimport { shadcn } from '@clerk/ui/themes';\n"
|
|
352
|
-
: '';
|
|
353
|
-
const providers = options.features.auth ? `function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
354
|
-
const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
|
355
|
-
if (!publishableKey) return <>{children}</>;
|
|
356
|
-
return <ClerkProvider publishableKey={publishableKey} appearance={{ theme: shadcn }}>{children}</ClerkProvider>;
|
|
357
|
-
}` : `function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
358
|
-
return <>{children}</>;
|
|
359
|
-
}`;
|
|
360
|
-
writeFile(path.join(dir, 'app/layout.tsx'), `import type { Metadata } from 'next';
|
|
361
|
-
${authImport}import './globals.css';
|
|
362
|
-
|
|
363
|
-
export const metadata: Metadata = {
|
|
364
|
-
title: ${displayNameLiteral},
|
|
365
|
-
description: ${descriptionLiteral},
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
${providers}
|
|
369
|
-
|
|
370
|
-
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
371
|
-
return (
|
|
372
|
-
<html lang="en">
|
|
373
|
-
<body>
|
|
374
|
-
<Providers>{children}</Providers>
|
|
375
|
-
</body>
|
|
376
|
-
</html>
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
`);
|
|
380
|
-
const subscriptionImport = options.features.billing ? "import { SubscriptionStatus } from '@/components/subscription-status';\n" : '';
|
|
381
|
-
const subscriptionStatus = options.features.billing
|
|
382
|
-
? ' {process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ? <SubscriptionStatus /> : null}\n'
|
|
383
|
-
: '';
|
|
384
|
-
writeFile(path.join(dir, 'app/page.tsx'), `${accountActionsImport}${subscriptionImport}import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
385
|
-
import { Button } from '@/components/ui/button';
|
|
386
|
-
|
|
387
|
-
export default function Home() {
|
|
388
|
-
return (
|
|
389
|
-
<main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-8 px-6 py-10">
|
|
390
|
-
<section className="flex flex-col gap-3">
|
|
391
|
-
<h1 className="text-4xl font-semibold tracking-normal">{${displayNameLiteral}}</h1>
|
|
392
|
-
<p className="max-w-2xl text-muted-foreground">
|
|
393
|
-
${options.apps.api ? 'Next.js + shadcn/ui web client connected to the shared Fastify API.' : 'Next.js + shadcn/ui web application.'}
|
|
394
|
-
</p>
|
|
395
|
-
<div className="flex gap-3">
|
|
396
|
-
<Button>Open app</Button>
|
|
397
|
-
${accountAction}
|
|
398
|
-
</div>
|
|
399
|
-
${subscriptionStatus} </section>
|
|
400
|
-
<Card>
|
|
401
|
-
<CardHeader>
|
|
402
|
-
<CardTitle>Shared modules</CardTitle>
|
|
403
|
-
</CardHeader>
|
|
404
|
-
<CardContent className="text-sm text-muted-foreground">
|
|
405
|
-
${options.apps.api ? 'Contracts, API access, and configuration are shared through workspace packages.' : 'Add modules later with anhedral add; user-owned source remains untouched.'}
|
|
406
|
-
</CardContent>
|
|
407
|
-
</Card>
|
|
408
|
-
</main>
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
`);
|
|
412
|
-
}
|
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;
|
package/dist/ui.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { TOOLCHAIN_DEPENDENCIES } from './dependencies.js';
|
|
4
|
-
import { execFile } from './util.js';
|
|
5
|
-
export const UI_TARGETS = ['web', 'mobile', 'desktop', 'extension'];
|
|
6
|
-
export const NATIVE_STYLING_LIBRARIES = ['nativewind', 'uniwind'];
|
|
7
|
-
export const UI_PROVIDERS = ['shadcn', 'react-native-reusables'];
|
|
8
|
-
const COMPONENT_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
9
|
-
const SOURCE_EXTENSION_PATTERN = /\.(?:[cm]?[jt]sx?)$/;
|
|
10
|
-
const IMPORT_SPECIFIER_PATTERN = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*)['"]([^'"]+)['"]/g;
|
|
11
|
-
function collectSourcePackageImports(root) {
|
|
12
|
-
const packages = new Set();
|
|
13
|
-
const visit = (directory) => {
|
|
14
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
15
|
-
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.next'))
|
|
16
|
-
continue;
|
|
17
|
-
const absolutePath = path.join(directory, entry.name);
|
|
18
|
-
if (entry.isDirectory()) {
|
|
19
|
-
visit(absolutePath);
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
if (!entry.isFile() || !SOURCE_EXTENSION_PATTERN.test(entry.name))
|
|
23
|
-
continue;
|
|
24
|
-
const source = readFileSync(absolutePath, 'utf8');
|
|
25
|
-
for (const match of source.matchAll(IMPORT_SPECIFIER_PATTERN)) {
|
|
26
|
-
const specifier = match[1];
|
|
27
|
-
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')
|
|
28
|
-
|| specifier.startsWith('@/') || specifier.startsWith('node:') || specifier.startsWith('virtual:'))
|
|
29
|
-
continue;
|
|
30
|
-
const segments = specifier.split('/');
|
|
31
|
-
packages.add(specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
visit(root);
|
|
36
|
-
return packages;
|
|
37
|
-
}
|
|
38
|
-
function workspacePackageManifests(workspaceRoot) {
|
|
39
|
-
const manifests = [];
|
|
40
|
-
for (const directory of [workspaceRoot, path.join(workspaceRoot, 'apps'), path.join(workspaceRoot, 'packages')]) {
|
|
41
|
-
if (!existsSync(directory))
|
|
42
|
-
continue;
|
|
43
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
44
|
-
const packagePath = entry.isDirectory()
|
|
45
|
-
? path.join(directory, entry.name, 'package.json')
|
|
46
|
-
: entry.name === 'package.json' ? path.join(directory, entry.name) : '';
|
|
47
|
-
if (!packagePath)
|
|
48
|
-
continue;
|
|
49
|
-
try {
|
|
50
|
-
manifests.push(JSON.parse(readFileSync(packagePath, 'utf8')));
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
if (error.code !== 'ENOENT')
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return manifests;
|
|
59
|
-
}
|
|
60
|
-
export function reconcileUiWorkspaceDependencies(workspaceRoot, targetDirectory) {
|
|
61
|
-
const packagePath = path.join(targetDirectory, 'package.json');
|
|
62
|
-
const target = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
63
|
-
const declared = new Set([
|
|
64
|
-
...Object.keys(target.dependencies ?? {}),
|
|
65
|
-
...Object.keys(target.devDependencies ?? {}),
|
|
66
|
-
...Object.keys(target.optionalDependencies ?? {}),
|
|
67
|
-
...Object.keys(target.peerDependencies ?? {}),
|
|
68
|
-
]);
|
|
69
|
-
const manifests = workspacePackageManifests(workspaceRoot);
|
|
70
|
-
const dependencies = { ...(target.dependencies ?? {}) };
|
|
71
|
-
let changed = false;
|
|
72
|
-
for (const packageName of collectSourcePackageImports(targetDirectory)) {
|
|
73
|
-
if (declared.has(packageName))
|
|
74
|
-
continue;
|
|
75
|
-
const version = manifests
|
|
76
|
-
.map((manifest) => manifest.dependencies?.[packageName])
|
|
77
|
-
.find((candidate) => typeof candidate === 'string');
|
|
78
|
-
if (!version)
|
|
79
|
-
continue;
|
|
80
|
-
dependencies[packageName] = version;
|
|
81
|
-
declared.add(packageName);
|
|
82
|
-
changed = true;
|
|
83
|
-
}
|
|
84
|
-
if (!changed)
|
|
85
|
-
return;
|
|
86
|
-
target.dependencies = Object.fromEntries(Object.entries(dependencies).sort(([left], [right]) => left.localeCompare(right)));
|
|
87
|
-
writeFileSync(packagePath, JSON.stringify(target, null, 2) + '\n');
|
|
88
|
-
}
|
|
89
|
-
export function isUiTarget(value) {
|
|
90
|
-
return typeof value === 'string' && UI_TARGETS.includes(value);
|
|
91
|
-
}
|
|
92
|
-
export function isNativeStylingLibrary(value) {
|
|
93
|
-
return typeof value === 'string' && NATIVE_STYLING_LIBRARIES.includes(value);
|
|
94
|
-
}
|
|
95
|
-
export function isUiProvider(value) {
|
|
96
|
-
return typeof value === 'string' && UI_PROVIDERS.includes(value);
|
|
97
|
-
}
|
|
98
|
-
export function normalizeUiComponentName(value) {
|
|
99
|
-
const normalized = value.trim().toLowerCase();
|
|
100
|
-
if (!COMPONENT_NAME_PATTERN.test(normalized)) {
|
|
101
|
-
throw new Error(`Invalid UI component name: ${value}. Use lowercase letters, numbers, and hyphens.`);
|
|
102
|
-
}
|
|
103
|
-
return normalized;
|
|
104
|
-
}
|
|
105
|
-
export function parseUiComponentList(value) {
|
|
106
|
-
return Array.from(new Set(value
|
|
107
|
-
.split(/[\s,]+/)
|
|
108
|
-
.map((entry) => entry.trim())
|
|
109
|
-
.filter(Boolean)
|
|
110
|
-
.map(normalizeUiComponentName)));
|
|
111
|
-
}
|
|
112
|
-
export function providerForTarget(target) {
|
|
113
|
-
return target === 'mobile' ? 'react-native-reusables' : 'shadcn';
|
|
114
|
-
}
|
|
115
|
-
export function registrySourceFor(name, target, nativeStyling) {
|
|
116
|
-
const component = normalizeUiComponentName(name);
|
|
117
|
-
return target === 'mobile'
|
|
118
|
-
? `https://reactnativereusables.com/r/${nativeStyling}/${component}.json`
|
|
119
|
-
: component;
|
|
120
|
-
}
|
|
121
|
-
export function resolveUiInstalls(componentNames, targets, nativeStyling) {
|
|
122
|
-
const components = Array.from(new Set(componentNames.map(normalizeUiComponentName))).sort();
|
|
123
|
-
const uniqueTargets = Array.from(new Set(targets)).sort();
|
|
124
|
-
return Object.freeze(uniqueTargets.flatMap((target) => components.map((name) => Object.freeze({
|
|
125
|
-
name,
|
|
126
|
-
target,
|
|
127
|
-
provider: providerForTarget(target),
|
|
128
|
-
source: registrySourceFor(name, target, nativeStyling),
|
|
129
|
-
variant: target === 'mobile' ? nativeStyling : null,
|
|
130
|
-
}))));
|
|
131
|
-
}
|
|
132
|
-
export function uiInstallKey(install) {
|
|
133
|
-
return `${install.target}:${install.name}`;
|
|
134
|
-
}
|
|
135
|
-
export function mergeUiInstalls(current, additions) {
|
|
136
|
-
const byKey = new Map(current.map((entry) => [uiInstallKey(entry), entry]));
|
|
137
|
-
for (const entry of additions)
|
|
138
|
-
byKey.set(uiInstallKey(entry), entry);
|
|
139
|
-
return Object.freeze([...byKey.values()].sort((left, right) => uiInstallKey(left).localeCompare(uiInstallKey(right))));
|
|
140
|
-
}
|
|
141
|
-
export function buildUiInstallCommands(root, installs) {
|
|
142
|
-
const commands = [];
|
|
143
|
-
for (const target of UI_TARGETS) {
|
|
144
|
-
const targetInstalls = installs.filter((entry) => entry.target === target);
|
|
145
|
-
if (targetInstalls.length === 0)
|
|
146
|
-
continue;
|
|
147
|
-
commands.push(Object.freeze({
|
|
148
|
-
target,
|
|
149
|
-
cwd: path.join(root, 'apps', target),
|
|
150
|
-
executable: 'pnpm',
|
|
151
|
-
args: Object.freeze([
|
|
152
|
-
'dlx',
|
|
153
|
-
`shadcn@${TOOLCHAIN_DEPENDENCIES.shadcn}`,
|
|
154
|
-
'add',
|
|
155
|
-
'--yes',
|
|
156
|
-
'--overwrite',
|
|
157
|
-
'--silent',
|
|
158
|
-
...targetInstalls.map((entry) => entry.source),
|
|
159
|
-
]),
|
|
160
|
-
installs: Object.freeze(targetInstalls),
|
|
161
|
-
}));
|
|
162
|
-
}
|
|
163
|
-
return Object.freeze(commands);
|
|
164
|
-
}
|
|
165
|
-
export function installUiComponents(root, installs) {
|
|
166
|
-
for (const command of buildUiInstallCommands(root, installs)) {
|
|
167
|
-
if (command.target !== 'mobile') {
|
|
168
|
-
execFile(command.executable, command.args, command.cwd);
|
|
169
|
-
reconcileUiWorkspaceDependencies(root, command.cwd);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
// shadcn switches to `npx expo install` whenever it sees an Expo dependency.
|
|
173
|
-
// Component generation runs in Anhedral's isolated stage before node_modules
|
|
174
|
-
// exists, so Expo CLI cannot determine the SDK. Temporarily mask only that
|
|
175
|
-
// detector field, let shadcn merge registry dependencies with pnpm, then
|
|
176
|
-
// restore Anhedral's exact Expo pin before the staged diff is committed.
|
|
177
|
-
const packagePath = path.join(command.cwd, 'package.json');
|
|
178
|
-
const originalText = readFileSync(packagePath, 'utf8');
|
|
179
|
-
const original = JSON.parse(originalText);
|
|
180
|
-
const expoVersion = original.dependencies?.expo;
|
|
181
|
-
if (!expoVersion)
|
|
182
|
-
throw new Error(`Expo dependency is missing from ${packagePath}`);
|
|
183
|
-
const masked = structuredClone(original);
|
|
184
|
-
delete masked.dependencies?.expo;
|
|
185
|
-
writeFileSync(packagePath, JSON.stringify(masked, null, 2) + '\n');
|
|
186
|
-
try {
|
|
187
|
-
execFile(command.executable, command.args, command.cwd);
|
|
188
|
-
const updated = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
189
|
-
updated.dependencies = { ...(updated.dependencies ?? {}), expo: expoVersion };
|
|
190
|
-
updated.dependencies = Object.fromEntries(Object.entries(updated.dependencies).sort(([left], [right]) => left.localeCompare(right)));
|
|
191
|
-
writeFileSync(packagePath, JSON.stringify(updated, null, 2) + '\n');
|
|
192
|
-
reconcileUiWorkspaceDependencies(root, command.cwd);
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
writeFileSync(packagePath, originalText);
|
|
196
|
-
throw error;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export type WorkspacePolicy = {
|
|
2
|
-
overrides: Record<string, string>;
|
|
3
|
-
onlyBuiltDependencies: string[];
|
|
4
|
-
ignoredBuiltDependencies: string[];
|
|
5
|
-
peerDependencyRules: {
|
|
6
|
-
ignoreMissing: string[];
|
|
7
|
-
allowedVersions: Record<string, string>;
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
export declare function desiredWorkspacePolicy(): WorkspacePolicy;
|
|
11
|
-
export declare function mergeWorkspaceFile(root: string, desiredPackages: string[], policy: WorkspacePolicy, allowGeneratedUpdates: boolean): void;
|