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
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { ProjectOptions } from '../
|
|
1
|
+
import type { ProjectOptions } from '../project.js';
|
|
2
2
|
export declare function scaffoldSharedPackages(root: string, options: ProjectOptions): void;
|
package/dist/templates/shared.js
CHANGED
|
@@ -108,14 +108,18 @@ export const realtimeOutbox = pgTable('realtime_outbox', {
|
|
|
108
108
|
` : '';
|
|
109
109
|
writeFile(path.join(dir, 'src/generated-schema.ts'), `import { integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
110
110
|
${storageTables}${billingTables}`);
|
|
111
|
-
|
|
111
|
+
const itemOwnerColumn = options.features.auth ? " userId: text('user_id').notNull(),\n" : '';
|
|
112
|
+
const itemIndexes = options.features.auth
|
|
113
|
+
? "}, (table) => [\n index('items_user_created_at_idx').on(table.userId, table.createdAt),\n]"
|
|
114
|
+
: '\n}';
|
|
115
|
+
writeFile(path.join(dir, 'src/app-schema.ts'), `import { ${options.features.auth ? 'index, ' : ''}pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
112
116
|
|
|
113
117
|
// Product-owned tables belong here. Anhedral never rewrites this file after creation.
|
|
114
118
|
export const items = pgTable('items', {
|
|
115
119
|
id: text('id').primaryKey(),
|
|
116
|
-
name: text('name').notNull(),
|
|
120
|
+
${itemOwnerColumn} name: text('name').notNull(),
|
|
117
121
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
118
|
-
});
|
|
122
|
+
${itemIndexes});
|
|
119
123
|
`);
|
|
120
124
|
writeFile(path.join(dir, 'src/schema.ts'), `export * from './generated-schema';
|
|
121
125
|
export * from './app-schema';
|
|
@@ -420,6 +424,62 @@ export class ApiClient {
|
|
|
420
424
|
}
|
|
421
425
|
`;
|
|
422
426
|
}
|
|
427
|
+
function appContractSource(options) {
|
|
428
|
+
if (!options.features.database) {
|
|
429
|
+
return `// Product-owned network contracts belong here.
|
|
430
|
+
// Anhedral never rewrites this file after creation.
|
|
431
|
+
export {};
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
return `import { z } from 'zod';
|
|
435
|
+
|
|
436
|
+
// A complete starter feature: edit or replace it with your product model.
|
|
437
|
+
export const ItemSchema = z.object({
|
|
438
|
+
id: z.string().uuid(),
|
|
439
|
+
name: z.string().trim().min(1).max(120),
|
|
440
|
+
createdAt: z.string().datetime(),
|
|
441
|
+
});
|
|
442
|
+
export const ItemListSchema = z.array(ItemSchema);
|
|
443
|
+
export const CreateItemRequestSchema = z.object({
|
|
444
|
+
name: z.string().trim().min(1).max(120),
|
|
445
|
+
}).strict();
|
|
446
|
+
|
|
447
|
+
export type Item = z.infer<typeof ItemSchema>;
|
|
448
|
+
export type CreateItemRequest = z.infer<typeof CreateItemRequestSchema>;
|
|
449
|
+
`;
|
|
450
|
+
}
|
|
451
|
+
function appApiClientSource(options) {
|
|
452
|
+
if (!options.features.database) {
|
|
453
|
+
return `// Product-owned API client helpers belong here.
|
|
454
|
+
// Anhedral never rewrites this file after creation.
|
|
455
|
+
export {};
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
return `import {
|
|
459
|
+
CreateItemRequestSchema,
|
|
460
|
+
ItemListSchema,
|
|
461
|
+
ItemSchema,
|
|
462
|
+
type CreateItemRequest,
|
|
463
|
+
} from '@shared/contracts';
|
|
464
|
+
import { ApiClient } from './generated';
|
|
465
|
+
|
|
466
|
+
export type { Item } from '@shared/contracts';
|
|
467
|
+
|
|
468
|
+
// These calls complete the starter feature across contracts, API, database, and clients.
|
|
469
|
+
export function listItems(client: ApiClient, init: RequestInit = {}) {
|
|
470
|
+
return client.request('/items', init, ItemListSchema);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function createItem(client: ApiClient, input: CreateItemRequest, init: RequestInit = {}) {
|
|
474
|
+
const body = CreateItemRequestSchema.parse(input);
|
|
475
|
+
return client.request('/items', {
|
|
476
|
+
...init,
|
|
477
|
+
method: 'POST',
|
|
478
|
+
body: JSON.stringify(body),
|
|
479
|
+
}, ItemSchema);
|
|
480
|
+
}
|
|
481
|
+
`;
|
|
482
|
+
}
|
|
423
483
|
function writeApiPackages(root, options) {
|
|
424
484
|
if (!options.apps.api)
|
|
425
485
|
return;
|
|
@@ -444,10 +504,7 @@ function writeApiPackages(root, options) {
|
|
|
444
504
|
}, null, 2) + '\n');
|
|
445
505
|
writeTsConfig(dir);
|
|
446
506
|
writeFile(path.join(dir, 'src/generated.ts'), source);
|
|
447
|
-
writeFile(path.join(dir, 'src/app.ts'),
|
|
448
|
-
// Anhedral never rewrites this file after creation.
|
|
449
|
-
export {};
|
|
450
|
-
`);
|
|
507
|
+
writeFile(path.join(dir, 'src/app.ts'), name === 'contracts' ? appContractSource(options) : appApiClientSource(options));
|
|
451
508
|
writeFile(path.join(dir, 'src/index.ts'), `export * from './generated';
|
|
452
509
|
export * from './app';
|
|
453
510
|
`);
|
package/dist/templates/web.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { ProjectOptions } from '../
|
|
1
|
+
import type { ProjectOptions } from '../project.js';
|
|
2
2
|
export declare function scaffoldWeb(root: string, options: ProjectOptions): Promise<void>;
|
package/dist/templates/web.js
CHANGED
|
@@ -137,10 +137,10 @@ function normalizeWebApiBaseUrl(value: string): string {
|
|
|
137
137
|
if (candidate.startsWith('//') || candidate.includes('\\\\')) {
|
|
138
138
|
throw new Error('NEXT_PUBLIC_API_URL must be a single-slash root-relative path or an absolute URL');
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return normalizeApiBaseUrl(new URL(candidate,
|
|
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
144
|
}
|
|
145
145
|
|
|
146
146
|
function apiBaseUrl(): string {
|
|
@@ -148,7 +148,8 @@ function apiBaseUrl(): string {
|
|
|
148
148
|
const candidate = configured || (process.env.NODE_ENV === 'development' ? 'http://localhost:8787/api' : '/api');
|
|
149
149
|
const normalized = normalizeWebApiBaseUrl(candidate);
|
|
150
150
|
const url = new URL(normalized);
|
|
151
|
-
|
|
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) {
|
|
152
153
|
throw new Error('NEXT_PUBLIC_API_URL must use https: in production');
|
|
153
154
|
}
|
|
154
155
|
return normalized;
|
|
@@ -161,8 +162,8 @@ export function createApiClient(getToken?: () => Promise<string | null>) {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
`);
|
|
164
|
-
if (options.apps.api
|
|
165
|
-
writeFile(path.join(dir, 'hooks/use-api-client.ts'), `'use client';
|
|
165
|
+
if (options.apps.api) {
|
|
166
|
+
writeFile(path.join(dir, 'hooks/use-api-client.ts'), options.features.auth ? `'use client';
|
|
166
167
|
|
|
167
168
|
import { useAuth } from '@clerk/nextjs';
|
|
168
169
|
import { useMemo } from 'react';
|
|
@@ -172,6 +173,120 @@ export function useApiClient() {
|
|
|
172
173
|
const { getToken } = useAuth();
|
|
173
174
|
return useMemo(() => createApiClient(() => getToken()), [getToken]);
|
|
174
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
|
+
}
|
|
175
290
|
`);
|
|
176
291
|
}
|
|
177
292
|
if (options.features.billing) {
|
|
@@ -185,39 +300,62 @@ import { useApiClient } from './use-api-client';
|
|
|
185
300
|
export function useEntitlement() {
|
|
186
301
|
const api = useApiClient();
|
|
187
302
|
const { isLoaded, isSignedIn, userId } = useAuth();
|
|
303
|
+
const identity = isLoaded && isSignedIn ? userId : null;
|
|
188
304
|
const [state, setState] = React.useState<Awaited<ReturnType<typeof api.getEntitlement>> | null>(null);
|
|
305
|
+
const [loadedIdentity, setLoadedIdentity] = React.useState<string | null>(null);
|
|
189
306
|
const [error, setError] = React.useState<string | null>(null);
|
|
190
307
|
const revision = React.useRef(0);
|
|
308
|
+
const identityRef = React.useRef(identity);
|
|
191
309
|
const refresh = React.useCallback(async () => {
|
|
192
|
-
if (!
|
|
310
|
+
if (!identity) return;
|
|
193
311
|
try {
|
|
194
312
|
const next = await api.getEntitlement();
|
|
313
|
+
if (identityRef.current !== identity) return;
|
|
195
314
|
revision.current = Math.max(revision.current, next.revision);
|
|
196
315
|
setState(next);
|
|
316
|
+
setLoadedIdentity(identity);
|
|
197
317
|
setError(null);
|
|
198
318
|
} catch (cause) {
|
|
319
|
+
if (identityRef.current !== identity) return;
|
|
320
|
+
setLoadedIdentity(identity);
|
|
199
321
|
setError(cause instanceof Error ? cause.message : 'Unable to load subscription');
|
|
200
322
|
}
|
|
201
|
-
}, [api,
|
|
323
|
+
}, [api, identity]);
|
|
202
324
|
|
|
203
|
-
React.useEffect(() => {
|
|
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]);
|
|
204
333
|
React.useEffect(() => {
|
|
205
334
|
const onVisibility = () => { if (document.visibilityState === 'visible') void refresh(); };
|
|
206
335
|
document.addEventListener('visibilitychange', onVisibility);
|
|
207
336
|
return () => document.removeEventListener('visibilitychange', onVisibility);
|
|
208
337
|
}, [refresh]);
|
|
209
338
|
React.useEffect(() => {
|
|
210
|
-
if (!
|
|
339
|
+
if (!identity) return;
|
|
211
340
|
return subscribeToSubscriptionChanges({
|
|
212
|
-
userId,
|
|
341
|
+
userId: identity,
|
|
213
342
|
getTokenRequest: () => api.getRealtimeToken(),
|
|
214
343
|
onChange: (nextRevision) => {
|
|
215
344
|
if (nextRevision > revision.current) void refresh();
|
|
216
345
|
},
|
|
217
|
-
onError: (cause) =>
|
|
346
|
+
onError: (cause) => {
|
|
347
|
+
if (identityRef.current === identity) {
|
|
348
|
+
setLoadedIdentity(identity);
|
|
349
|
+
setError(cause.message);
|
|
350
|
+
}
|
|
351
|
+
},
|
|
218
352
|
});
|
|
219
|
-
}, [api,
|
|
220
|
-
return {
|
|
353
|
+
}, [api, identity, refresh]);
|
|
354
|
+
return {
|
|
355
|
+
entitlement: loadedIdentity === identity ? state : null,
|
|
356
|
+
error: loadedIdentity === identity ? error : null,
|
|
357
|
+
refresh,
|
|
358
|
+
};
|
|
221
359
|
}
|
|
222
360
|
`);
|
|
223
361
|
writeFile(path.join(dir, 'components/subscription-status.tsx'), `'use client';
|
|
@@ -303,12 +441,22 @@ function writeAppFiles(dir, options) {
|
|
|
303
441
|
? "import { AccountActions } from '@/components/account-actions';\n"
|
|
304
442
|
: '';
|
|
305
443
|
const accountAction = options.features.auth
|
|
306
|
-
? `{process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ? (
|
|
444
|
+
? `{process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY && !process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.includes('***') ? (
|
|
307
445
|
<AccountActions />
|
|
308
446
|
) : (
|
|
309
447
|
<Button variant="outline" disabled>Configure Clerk to sign in</Button>
|
|
310
448
|
)}`
|
|
311
|
-
: '
|
|
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
|
+
: '';
|
|
312
460
|
const clerkStyles = options.features.auth ? '@import "@clerk/ui/themes/shadcn.css";\n' : '';
|
|
313
461
|
writeFile(path.join(dir, 'app/globals.css'), `@import "tailwindcss";
|
|
314
462
|
${clerkStyles}
|
|
@@ -352,7 +500,7 @@ body {
|
|
|
352
500
|
: '';
|
|
353
501
|
const providers = options.features.auth ? `function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
354
502
|
const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
|
355
|
-
if (!publishableKey) return <>{children}</>;
|
|
503
|
+
if (!publishableKey || publishableKey.includes('***')) return <>{children}</>;
|
|
356
504
|
return <ClerkProvider publishableKey={publishableKey} appearance={{ theme: shadcn }}>{children}</ClerkProvider>;
|
|
357
505
|
}` : `function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
358
506
|
return <>{children}</>;
|
|
@@ -378,11 +526,23 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
|
|
|
378
526
|
}
|
|
379
527
|
`);
|
|
380
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" : '';
|
|
381
530
|
const subscriptionStatus = options.features.billing
|
|
382
|
-
?
|
|
531
|
+
? " {process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY && !process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.includes('***') ? <SubscriptionStatus /> : null}\n"
|
|
383
532
|
: '';
|
|
384
|
-
|
|
385
|
-
|
|
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}
|
|
386
546
|
|
|
387
547
|
export default function Home() {
|
|
388
548
|
return (
|
|
@@ -392,17 +552,14 @@ export default function Home() {
|
|
|
392
552
|
<p className="max-w-2xl text-muted-foreground">
|
|
393
553
|
${options.apps.api ? 'Next.js + shadcn/ui web client connected to the shared Fastify API.' : 'Next.js + shadcn/ui web application.'}
|
|
394
554
|
</p>
|
|
395
|
-
|
|
396
|
-
<Button>Open app</Button>
|
|
397
|
-
${accountAction}
|
|
398
|
-
</div>
|
|
555
|
+
${heroActions}
|
|
399
556
|
${subscriptionStatus} </section>
|
|
400
|
-
<Card>
|
|
557
|
+
<Card id="starter-feature">
|
|
401
558
|
<CardHeader>
|
|
402
|
-
<CardTitle
|
|
559
|
+
<CardTitle>${starterTitle}</CardTitle>
|
|
403
560
|
</CardHeader>
|
|
404
561
|
<CardContent className="text-sm text-muted-foreground">
|
|
405
|
-
${
|
|
562
|
+
${starterContent}
|
|
406
563
|
</CardContent>
|
|
407
564
|
</Card>
|
|
408
565
|
</main>
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Kept in sync with package.json by tests/release checks. */
|
|
2
|
-
export declare const GENERATOR_VERSION = "0.4.
|
|
2
|
+
export declare const GENERATOR_VERSION = "0.4.2";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Kept in sync with package.json by tests/release checks. */
|
|
2
|
-
export const GENERATOR_VERSION = '0.4.
|
|
2
|
+
export const GENERATOR_VERSION = '0.4.2';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Anhedral DX north star
|
|
2
|
+
|
|
3
|
+
## One command in. A working product foundation out.
|
|
4
|
+
|
|
5
|
+
Anhedral should let a developer choose the surfaces and capabilities their idea
|
|
6
|
+
needs, generate one understandable TypeScript workspace, and reach a working
|
|
7
|
+
feature without first becoming an expert in every framework and provider.
|
|
8
|
+
|
|
9
|
+
The generated application must feel like a project the developer wrote
|
|
10
|
+
themselves:
|
|
11
|
+
|
|
12
|
+
- ordinary framework source, package scripts, environment files, and tests;
|
|
13
|
+
- one shared contract and API client across every selected surface;
|
|
14
|
+
- working vertical slices instead of disconnected demonstrations;
|
|
15
|
+
- safe, inspectable structural changes with no hidden runtime or control plane;
|
|
16
|
+
- a clear path from first run to production without changing architecture; and
|
|
17
|
+
- built-in scale through managed, autoscaling providers rather than custom
|
|
18
|
+
infrastructure.
|
|
19
|
+
|
|
20
|
+
Anhedral succeeds when developers spend their time on the product idea, not on
|
|
21
|
+
repeating setup, reconciling integrations, or reverse-engineering generated
|
|
22
|
+
code.
|
|
23
|
+
|
|
24
|
+
## Product principles
|
|
25
|
+
|
|
26
|
+
### Fast to first value
|
|
27
|
+
|
|
28
|
+
A developer should understand the choices before generation, see the resolved
|
|
29
|
+
dependency closure, and know the next command when generation finishes.
|
|
30
|
+
`pnpm first-run` prepares local configuration without overwriting it.
|
|
31
|
+
`pnpm ready` answers whether required configuration is complete without
|
|
32
|
+
revealing values. `pnpm verify` proves the source and selected integrations
|
|
33
|
+
build.
|
|
34
|
+
|
|
35
|
+
### Build once, ship everywhere
|
|
36
|
+
|
|
37
|
+
Contracts, validation, API calls, authentication, persistence, and product
|
|
38
|
+
behavior should be shared where the platforms permit it. Every selected client
|
|
39
|
+
should receive an idiomatic UI for the same starter feature so the developer can
|
|
40
|
+
prove the whole path on web, iOS, Android, desktop, and browser extension
|
|
41
|
+
surfaces before replacing it with their idea.
|
|
42
|
+
|
|
43
|
+
### Explicit composition
|
|
44
|
+
|
|
45
|
+
The CLI must distinguish what the developer requested from what dependencies
|
|
46
|
+
added. Interactive and automated use must produce the same canonical module
|
|
47
|
+
closure. Complete-stack generation is explicit with `--all`; focused stacks
|
|
48
|
+
remain first-class.
|
|
49
|
+
|
|
50
|
+
### Safe evolution
|
|
51
|
+
|
|
52
|
+
`add`, `ui add`, and `upgrade` must be transactional, previewable, deterministic,
|
|
53
|
+
and ownership-aware. They may merge known configuration and replace unmodified
|
|
54
|
+
managed substrate, but must never silently overwrite product code. `doctor`
|
|
55
|
+
must explain drift and give a concrete recovery action.
|
|
56
|
+
|
|
57
|
+
### Production-shaped from the start
|
|
58
|
+
|
|
59
|
+
Development and production use the same package boundaries and provider
|
|
60
|
+
integrations. Environment examples may contain placeholders, but generated
|
|
61
|
+
applications must identify them before launch. Secrets stay server-side and
|
|
62
|
+
uncommitted. Deployment guidance is specific to the selected stack and does not
|
|
63
|
+
claim resources already exist.
|
|
64
|
+
|
|
65
|
+
### Human and automation parity
|
|
66
|
+
|
|
67
|
+
Human output should be concise and actionable. `--json` should provide one clean,
|
|
68
|
+
stable document with the same resolved modules, plans, outcomes, and next steps
|
|
69
|
+
for CI and coding agents. Noninteractive commands must never depend on a prompt.
|
|
70
|
+
|
|
71
|
+
### Source ownership is the feature
|
|
72
|
+
|
|
73
|
+
Generated product seams are user-owned and readable. Provider substrate is
|
|
74
|
+
managed only where consistency and security require it. The manifest records
|
|
75
|
+
the boundary, and every lifecycle command honors it.
|
|
76
|
+
|
|
77
|
+
## Journey contract
|
|
78
|
+
|
|
79
|
+
| Journey | The developer should experience | Acceptance signal |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| Discover | Immediately understand cross-platform scope, modularity, cost posture, and ownership | README states the value plainly and links to exact commands |
|
|
82
|
+
| Choose | See app surfaces, capabilities, dependencies, and runtime requirements before writes | Interactive confirmation and `--help` show the canonical resolved stack |
|
|
83
|
+
| Create | Receive deterministic source, optional Git initialization, visible installation progress, and exact next steps | `new`, `init`, dry-run, JSON, skip-install, and Git variants are tested |
|
|
84
|
+
| Configure | Safely create only missing local environment files and identify every unresolved placeholder without printing secrets | `pnpm first-run` is idempotent; `pnpm ready` returns a truthful exit status |
|
|
85
|
+
| Prove | Exercise a real contract → database → API → client → UI feature on every selected surface | Focused and complete generated stacks typecheck, test, and build |
|
|
86
|
+
| Develop | Know where product code belongs and use each framework normally | Generated README, development guide, stack map, and coding-agent skill agree |
|
|
87
|
+
| Extend | Preview and add modules or source-owned UI without losing edits | Dry-run and apply plans are deterministic, transactional, and ownership-safe |
|
|
88
|
+
| Diagnose | Understand generator drift, configuration readiness, and recovery actions | `pnpm ready` reports configuration blockers; `doctor` reports structural issues and actionable recommendations |
|
|
89
|
+
| Upgrade | Move supported projects forward without replacing product seams | Upgrade is transactional, no-op when current, and tested from supported versions |
|
|
90
|
+
| Ship | Follow selection-specific steps from previews to production and stores | Generated production guide and exact package scripts cover every selected target |
|
|
91
|
+
| Automate | Consume stable output and releases without scraping terminal prose | JSON remains parseable; successful `main` CI prepares the next patch release |
|
|
92
|
+
|
|
93
|
+
## Definition of done
|
|
94
|
+
|
|
95
|
+
A change is complete only when:
|
|
96
|
+
|
|
97
|
+
1. focused and complete module topologies generate deterministically;
|
|
98
|
+
2. generated environment setup is idempotent and readiness checks are truthful;
|
|
99
|
+
3. the shared starter feature reaches every selected compatible client;
|
|
100
|
+
4. generated code typechecks, tests, builds, and passes runtime acceptance where
|
|
101
|
+
the platform is available;
|
|
102
|
+
5. dry-run, JSON, no-op, failure, rollback, and recovery paths are covered;
|
|
103
|
+
6. source ownership survives `add`, `ui add`, and `upgrade`;
|
|
104
|
+
7. packed npm output behaves the same as repository source;
|
|
105
|
+
8. security, dependency, secret, PII, source-hygiene, and release-policy checks
|
|
106
|
+
pass; and
|
|
107
|
+
9. documentation describes the behavior that was actually verified.
|
|
108
|
+
|
|
109
|
+
## Deliberate boundaries
|
|
110
|
+
|
|
111
|
+
Anhedral does not hide provider accounts, fabricate credentials, submit paid or
|
|
112
|
+
review-gated releases, run a proprietary application runtime, or own product
|
|
113
|
+
code after generation. Those boundaries keep the output understandable,
|
|
114
|
+
portable, and under the developer's control.
|
package/docs/conventions.md
CHANGED
|
@@ -60,8 +60,8 @@ Clerk owns identity and sessions. The API derives identity from a verified serve
|
|
|
60
60
|
DOM clients use source-owned shadcn/ui components. Expo uses React Native Reusables with the selected NativeWind or Uniwind configuration.
|
|
61
61
|
|
|
62
62
|
```sh
|
|
63
|
-
anhedral
|
|
64
|
-
anhedral
|
|
63
|
+
pnpm anhedral:ui button dialog
|
|
64
|
+
pnpm anhedral:ui data-table --target web
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Generated UI files are application source and can be customized. Preserve accessibility, keyboard/focus behavior, and platform conventions.
|
|
@@ -76,7 +76,7 @@ Generated UI files are application source and can be customized. Preserve access
|
|
|
76
76
|
|
|
77
77
|
## Structural changes
|
|
78
78
|
|
|
79
|
-
Run `anhedral
|
|
79
|
+
Run `pnpm anhedral:doctor` before changing generated structure. Preview new modules or UI with `--dry-run`. Anhedral refuses managed-file drift, symlinks, and unowned collisions instead of overwriting source.
|
|
80
80
|
|
|
81
81
|
Application product files are developer-owned. `README.md` and `PRODUCTION.md` become user-owned after initialization. Root configuration is mergeable. Integration substrate and generated guidance are recorded in `anhedral.json`.
|
|
82
82
|
|
|
@@ -85,8 +85,10 @@ Application product files are developer-owned. `README.md` and `PRODUCTION.md` b
|
|
|
85
85
|
Run the smallest relevant package check while iterating, then the generated root verification before handoff:
|
|
86
86
|
|
|
87
87
|
```sh
|
|
88
|
+
pnpm first-run
|
|
89
|
+
pnpm ready
|
|
88
90
|
pnpm typecheck
|
|
89
91
|
pnpm verify
|
|
90
92
|
pnpm build
|
|
91
|
-
anhedral
|
|
93
|
+
pnpm anhedral:doctor
|
|
92
94
|
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# The Zen of Anhedral
|
|
2
|
+
|
|
3
|
+
Anhedral applies the spirit of Tim Peters' *Zen of Python* to a TypeScript stack
|
|
4
|
+
generator. This is an engineering policy, not a preference for Python syntax.
|
|
5
|
+
The goal is generated software whose behavior, ownership, and failure modes are
|
|
6
|
+
easy to see.
|
|
7
|
+
|
|
8
|
+
## Working rules
|
|
9
|
+
|
|
10
|
+
1. **Make the result readable.** Generated source should look like ordinary,
|
|
11
|
+
idiomatic framework code. Prefer clear names and small seams over clever
|
|
12
|
+
abstractions or compressed templates.
|
|
13
|
+
2. **Make choices explicit.** Keep requested modules distinct from dependencies
|
|
14
|
+
added by resolution. Record ownership, toolchain, template provenance, and
|
|
15
|
+
planned writes. Never infer permission to replace product code.
|
|
16
|
+
3. **Keep one source of truth.** Module identity and kind belong to the
|
|
17
|
+
architecture registry. Project shape belongs to the project model. CLI,
|
|
18
|
+
templates, manifests, and docs consume those definitions instead of
|
|
19
|
+
restating them.
|
|
20
|
+
4. **Choose the smallest complete design.** A simple vertical slice is better
|
|
21
|
+
than a generic subsystem. Real provider complexity may be necessary, but it
|
|
22
|
+
must stay behind a narrow, named boundary.
|
|
23
|
+
5. **Prefer flat control flow.** Validate early, return early, and dispatch once.
|
|
24
|
+
Split orchestration, domain rules, rendering, filesystem transactions, and
|
|
25
|
+
process execution into distinct namespaces.
|
|
26
|
+
6. **Generate sparsely.** A selected stack receives what it needs. Unselected
|
|
27
|
+
apps and providers must not leave dead packages, environment variables,
|
|
28
|
+
scripts, or documentation behind.
|
|
29
|
+
7. **Treat rules uniformly.** Add module dependencies and conflicts to the
|
|
30
|
+
registry; do not scatter special-case resolution across commands. Introduce
|
|
31
|
+
an exception only when the practical behavior is clearer and it is covered
|
|
32
|
+
by a regression test.
|
|
33
|
+
8. **Never hide failure.** Invalid input, drift, child-process failure, partial
|
|
34
|
+
configuration, and unsafe writes must produce a useful error and nonzero
|
|
35
|
+
status. An intentionally ignored failure must be named, local, and tested.
|
|
36
|
+
9. **Do not guess through ambiguity.** Reject unknown commands, conflicting
|
|
37
|
+
edits, unowned collisions, unsafe paths, and unsupported project versions.
|
|
38
|
+
Interactive choices require confirmation; noninteractive behavior must be
|
|
39
|
+
deterministic.
|
|
40
|
+
10. **Provide one obvious path.** Use `new` or `init` to create, `add` for
|
|
41
|
+
modules, `ui add` for source-owned UI, `upgrade` for schema evolution, and
|
|
42
|
+
`doctor` for drift. Generated projects use `first-run`, `ready`, `verify`,
|
|
43
|
+
and then `dev`.
|
|
44
|
+
11. **Ship deliberate increments.** Finish a small explainable change now when
|
|
45
|
+
its behavior is proven. Defer a broad abstraction until it has a concrete
|
|
46
|
+
caller and an acceptance test.
|
|
47
|
+
12. **Make explanation a design test.** If a change cannot be summarized as
|
|
48
|
+
input → decision → observable result, simplify it before merging.
|
|
49
|
+
|
|
50
|
+
## Namespace boundaries
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
CLI parsing and presentation
|
|
54
|
+
-> architecture and project model
|
|
55
|
+
-> scaffold orchestration and transactions
|
|
56
|
+
-> templates and configuration composers
|
|
57
|
+
-> generated framework source
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `src/architecture/` owns module resolution, plans, manifests, and composition
|
|
61
|
+
invariants.
|
|
62
|
+
- `src/project.ts` owns the template-facing project shape.
|
|
63
|
+
- `src/scaffold.ts` coordinates lifecycle operations; it must not become the
|
|
64
|
+
source of domain types or embed unrelated templates.
|
|
65
|
+
- `src/templates/` renders selected framework and lifecycle files.
|
|
66
|
+
- `src/transaction.ts` owns safe application and rollback.
|
|
67
|
+
- `src/bin.ts`, `src/cli.ts`, and `src/prompts.ts` own user input and output.
|
|
68
|
+
|
|
69
|
+
Dependencies point toward the model, not back toward the orchestrator. A
|
|
70
|
+
template may depend on `ProjectOptions`; it should not depend on the scaffold
|
|
71
|
+
command that called it.
|
|
72
|
+
|
|
73
|
+
## Review questions
|
|
74
|
+
|
|
75
|
+
Before merging, answer these plainly:
|
|
76
|
+
|
|
77
|
+
- Where is the single authoritative definition for this behavior?
|
|
78
|
+
- What input is explicit, what is derived, and is that distinction observable?
|
|
79
|
+
- Does the happy path read top-to-bottom without hidden mutation?
|
|
80
|
+
- What ambiguity or failure can occur, and how does the user see it?
|
|
81
|
+
- Does the change add output for an unselected module?
|
|
82
|
+
- Can an existing user-owned file be overwritten or silently reclassified?
|
|
83
|
+
- Is there a focused test for the rule and a broader test for the journey?
|
|
84
|
+
- Can the implementation be explained in a short paragraph?
|
|
85
|
+
|
|
86
|
+
Practicality still wins: duplicated literal output inside generated framework
|
|
87
|
+
code can be clearer than a generator-only abstraction. The burden is on an
|
|
88
|
+
abstraction to remove real ambiguity or repetition without hiding behavior.
|