anhedral 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +68 -55
  4. package/SKILL.md +107 -12
  5. package/dist/architecture/index.d.ts +2 -2
  6. package/dist/architecture/index.js +1 -1
  7. package/dist/architecture/modules.d.ts +5 -1
  8. package/dist/architecture/modules.js +4 -1
  9. package/dist/bin.js +113 -84
  10. package/dist/cli.d.ts +7 -7
  11. package/dist/cli.js +62 -16
  12. package/dist/dependencies.d.ts +1 -0
  13. package/dist/dependencies.js +11 -8
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +1 -1
  16. package/dist/project.d.ts +30 -0
  17. package/dist/project.js +1 -0
  18. package/dist/prompts.d.ts +4 -3
  19. package/dist/prompts.js +29 -7
  20. package/dist/scaffold.d.ts +12 -24
  21. package/dist/scaffold.js +330 -123
  22. package/dist/templates/api.d.ts +1 -1
  23. package/dist/templates/api.js +67 -2
  24. package/dist/templates/assets-private-proxy.d.ts +1 -1
  25. package/dist/templates/desktop.d.ts +1 -1
  26. package/dist/templates/desktop.js +189 -36
  27. package/dist/templates/electron-updater.d.ts +1 -1
  28. package/dist/templates/electron-updater.js +1 -1
  29. package/dist/templates/extension.d.ts +1 -1
  30. package/dist/templates/extension.js +138 -14
  31. package/dist/templates/first-run.d.ts +2 -0
  32. package/dist/templates/first-run.js +167 -0
  33. package/dist/templates/mobile.d.ts +1 -1
  34. package/dist/templates/mobile.js +158 -17
  35. package/dist/templates/shared.d.ts +1 -1
  36. package/dist/templates/shared.js +64 -7
  37. package/dist/templates/web.d.ts +1 -1
  38. package/dist/templates/web.js +185 -28
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/docs/NORTHSTAR.md +114 -0
  42. package/docs/conventions.md +6 -4
  43. package/docs/engineering-principles.md +88 -0
  44. package/docs/output-tree-contract.md +16 -5
  45. package/docs/references/manual-scaffolding.md +22 -4
  46. package/docs/references/provisioning.md +434 -77
  47. package/package.json +3 -1
@@ -1,2 +1,2 @@
1
- import type { ProjectOptions } from '../scaffold.js';
1
+ import type { ProjectOptions } from '../project.js';
2
2
  export declare function scaffoldSharedPackages(root: string, options: ProjectOptions): void;
@@ -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
- writeFile(path.join(dir, 'src/app-schema.ts'), `import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
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'), `// Product-owned ${name === 'contracts' ? 'network contracts' : 'API client helpers'} belong here.
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
  `);
@@ -1,2 +1,2 @@
1
- import type { ProjectOptions } from '../scaffold.js';
1
+ import type { ProjectOptions } from '../project.js';
2
2
  export declare function scaffoldWeb(root: string, options: ProjectOptions): Promise<void>;
@@ -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
- 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');
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
- if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:') {
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 && options.features.auth) {
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 (!isLoaded || !isSignedIn) return;
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, isLoaded, isSignedIn]);
323
+ }, [api, identity]);
202
324
 
203
- React.useEffect(() => { void refresh(); }, [refresh]);
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 (!userId || !isSignedIn) return;
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) => setError(cause.message),
346
+ onError: (cause) => {
347
+ if (identityRef.current === identity) {
348
+ setLoadedIdentity(identity);
349
+ setError(cause.message);
350
+ }
351
+ },
218
352
  });
219
- }, [api, isSignedIn, refresh, userId]);
220
- return { entitlement: state, error, refresh };
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
- : '<Button variant="outline">View account</Button>';
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
- ? ' {process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ? <SubscriptionStatus /> : null}\n'
531
+ ? " {process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY && !process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.includes('***') ? <SubscriptionStatus /> : null}\n"
383
532
  : '';
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';
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
- <div className="flex gap-3">
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>Shared modules</CardTitle>
559
+ <CardTitle>${starterTitle}</CardTitle>
403
560
  </CardHeader>
404
561
  <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.'}
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.1";
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.1';
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.
@@ -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 ui add button dialog
64
- anhedral ui add data-table --target web
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 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.
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 doctor
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.