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
package/dist/scaffold.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { chmodSync, copyFileSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmdirSync, rmSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
2
3
  import path from 'node:path';
3
4
  import { env } from 'node:process';
4
5
  import { anhedralPrint } from './print.js';
@@ -10,6 +11,7 @@ import { r2BucketName, scaffoldAssetsPrivateProxy } from './templates/assets-pri
10
11
  import { scaffoldDesktop } from './templates/desktop.js';
11
12
  import { desktopUpdatesBucketName, scaffoldElectronUpdater, } from './templates/electron-updater.js';
12
13
  import { scaffoldExtension } from './templates/extension.js';
14
+ import { generatedFirstRunScript } from './templates/first-run.js';
13
15
  import { scaffoldMobile } from './templates/mobile.js';
14
16
  import { scaffoldSharedPackages } from './templates/shared.js';
15
17
  import { scaffoldWeb } from './templates/web.js';
@@ -21,7 +23,7 @@ import { collectModuleContributions } from './architecture/contributions.js';
21
23
  import { composeRootEnvironment, composeVercelCrons } from './composers/root.js';
22
24
  import { assertTemplateProvenance, materializeTemplates, } from './template-source.js';
23
25
  import { desiredWorkspacePolicy, mergeWorkspaceFile, } from './workspace-config.js';
24
- import { UI_TARGETS, installUiComponents, mergeUiInstalls, resolveUiInstalls, uiInstallKey, } from './ui.js';
26
+ import { UI_TARGETS, buildUiInstallCommands, installUiComponents, mergeUiInstalls, resolveUiInstalls, uiInstallKey, } from './ui.js';
25
27
  export function supportsMobileInstallNode(version) {
26
28
  const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
27
29
  if (!match)
@@ -213,12 +215,18 @@ function rootScripts(options) {
213
215
  ? [primaryClient, ...(options.apps.api ? ['./apps/api'] : [])]
214
216
  : filters.slice(0, 1);
215
217
  const scripts = {
216
- dev: primaryFilters.length ? `turbo dev --parallel ${primaryFilters.map((entry) => `--filter=${entry}`).join(' ')}` : 'echo "No app surfaces selected."',
218
+ 'first-run': 'node scripts/first-run.mjs',
219
+ ready: 'node scripts/first-run.mjs --check',
220
+ dev: primaryFilters.length ? `turbo run dev ${primaryFilters.map((entry) => `--filter=${entry}`).join(' ')}` : 'echo "No app surfaces selected."',
217
221
  ...(filters.length > primaryFilters.length
218
- ? { 'dev:all': `turbo dev --parallel ${filters.map((entry) => `--filter=${entry}`).join(' ')}` }
222
+ ? { 'dev:all': `turbo run dev ${filters.map((entry) => `--filter=${entry}`).join(' ')}` }
219
223
  : {}),
220
224
  build: 'turbo build',
221
225
  typecheck: 'turbo typecheck',
226
+ 'anhedral:doctor': `pnpm dlx anhedral@${GENERATOR_VERSION} doctor`,
227
+ 'anhedral:add': `pnpm dlx anhedral@${GENERATOR_VERSION} add`,
228
+ 'anhedral:ui': `pnpm dlx anhedral@${GENERATOR_VERSION} ui add`,
229
+ 'anhedral:upgrade': 'pnpm dlx anhedral@latest upgrade',
222
230
  };
223
231
  const verify = [];
224
232
  if (options.apps.web) {
@@ -523,15 +531,22 @@ function writeRootEnv(root, options) {
523
531
  const separator = current && additions.length ? (current.endsWith('\n') ? '\n' : '\n\n') : '';
524
532
  writeFile(filePath, `${current}${separator}${additions.join('\n')}${additions.length ? '\n' : ''}`);
525
533
  }
526
- function desiredTurboConfig() {
534
+ function desiredTurboConfig(options) {
535
+ const appBuildTasks = Object.fromEntries([
536
+ options.apps.web ? [`${options.projectName}-web#build`, { outputs: ['.next/**', '!.next/cache/**'] }] : null,
537
+ options.apps.mobile ? [`${options.projectName}-mobile#build`, { outputs: ['dist/**'] }] : null,
538
+ options.apps.desktop ? [`${options.projectName}-desktop#build`, { outputs: ['dist/**'] }] : null,
539
+ options.apps.extension ? [`${options.projectName}-chrome-ext#build`, { outputs: ['.output/**'] }] : null,
540
+ ].filter((entry) => entry !== null));
527
541
  return {
528
542
  $schema: 'https://turborepo.dev/schema.json',
529
543
  tasks: {
530
544
  build: {
531
545
  dependsOn: ['^build'],
532
- outputs: ['.next/**', '!.next/cache/**', '.output/**', 'dist/**'],
546
+ outputs: [],
533
547
  },
534
- typecheck: { dependsOn: ['^build'] },
548
+ ...appBuildTasks,
549
+ typecheck: { dependsOn: ['^typecheck'] },
535
550
  dev: { cache: false, persistent: true },
536
551
  },
537
552
  };
@@ -690,7 +705,7 @@ function writeRootFiles(root, options, mode, currentRoot, manifest) {
690
705
  options.apps.api || options.features.database ? 'packages/*' : null,
691
706
  ].filter((value) => value !== null);
692
707
  mergeWorkspaceFile(root, workspacePackages, desiredWorkspacePolicy(), workspaceUnmodified);
693
- mergeJsonConfig(root, 'turbo.json', desiredTurboConfig(), desiredTurboConfig(), true);
708
+ mergeJsonConfig(root, 'turbo.json', desiredTurboConfig(options), desiredTurboConfig(options), true);
694
709
  mergeJsonConfig(root, 'vercel.json', desiredVercelConfig(options), previousOptions ? desiredVercelConfig(previousOptions) : {}, true);
695
710
  appendGitignore(root, [
696
711
  'node_modules',
@@ -721,6 +736,7 @@ function writeRootFiles(root, options, mode, currentRoot, manifest) {
721
736
  'apps/desktop-updater-worker',
722
737
  ])];
723
738
  writeFile(vercelIgnore, ignoreLines.join('\n') + '\n');
739
+ writeFile(path.join(root, 'scripts/first-run.mjs'), generatedFirstRunScript(options));
724
740
  writeFile(path.join(root, '.github/workflows/anhedral-ci.yml'), generatedCi(options));
725
741
  }
726
742
  function enabledModuleNames(options) {
@@ -729,17 +745,11 @@ function enabledModuleNames(options) {
729
745
  function writeProjectDocs(root, options, includeUserDocs) {
730
746
  const modules = enabledModuleNames(options);
731
747
  const storageBucketName = r2BucketName(options.projectName);
732
- const environmentSetupCommands = [
733
- 'pnpm install',
734
- options.apps.api ? 'cp apps/api/.env.example apps/api/.env' : null,
735
- options.features.database ? 'cp packages/db/.env.example packages/db/.env' : null,
736
- options.apps.web ? 'cp apps/web/.env.example apps/web/.env.local' : null,
737
- options.apps.mobile ? 'cp apps/mobile/.env.example apps/mobile/.env' : null,
738
- options.apps.desktop ? 'cp apps/desktop/.env.example apps/desktop/.env' : null,
739
- options.features.electronUpdater ? 'cp apps/desktop/electron-builder.env.example apps/desktop/electron-builder.env' : null,
740
- options.apps.extension ? 'cp apps/extension/.env.example apps/extension/.env' : null,
741
- ].filter((value) => value !== null).join('\n');
748
+ const environmentSetupCommands = `pnpm install # only if generation used --skip-install
749
+ pnpm first-run
750
+ pnpm ready`;
742
751
  const firstVerificationCommands = [
752
+ options.features.database ? 'git rev-parse --is-inside-work-tree >/dev/null 2>&1 || git init' : null,
743
753
  options.features.database ? 'pnpm db:generate' : null,
744
754
  options.features.database ? 'git add packages/db/migrations' : null,
745
755
  'pnpm verify',
@@ -761,14 +771,14 @@ function writeProjectDocs(root, options, includeUserDocs) {
761
771
  options.apps.mobile ? 'pnpm mobile:eas:login' : null,
762
772
  options.apps.mobile ? 'pnpm mobile:build:internal:ios' : null,
763
773
  options.apps.mobile ? 'pnpm mobile:build:internal:android' : null,
764
- options.features.database ? `pnpm neon:project:create -- --name ${options.projectName}` : null,
774
+ options.features.database ? `pnpm neon:project:create --name ${options.projectName}` : null,
765
775
  options.features.storage ? 'pnpm r2:bucket:create' : null,
766
776
  options.features.storage ? 'pnpm assets:proxy:check' : null,
767
777
  options.features.storage ? 'pnpm assets:proxy:deploy' : null,
768
778
  options.features.electronUpdater ? 'pnpm desktop:updates:cloudflare:login' : null,
769
779
  options.features.electronUpdater ? 'pnpm desktop:updates:first-provision' : null,
770
780
  options.features.electronUpdater ? 'pnpm desktop:updates:build:mac' : null,
771
- options.features.electronUpdater ? 'pnpm desktop:updates:publish -- --platform mac --arch arm64' : null,
781
+ options.features.electronUpdater ? 'pnpm desktop:updates:publish --platform mac --arch arm64' : null,
772
782
  options.apps.extension ? 'pnpm extension:zip' : null,
773
783
  ].filter((value) => value !== null).join('\n');
774
784
  const sourceRows = [
@@ -828,11 +838,12 @@ Edit \`packages/db/src/app-schema.ts\`, then run:
828
838
 
829
839
  \`\`\`sh
830
840
  pnpm db:generate
841
+ git add packages/db/migrations
831
842
  pnpm verify:db
832
843
  pnpm db:migrate
833
844
  \`\`\`
834
845
 
835
- Review generated SQL before applying it. \`DATABASE_URL\` points to a managed Neon branch or project; there is intentionally no local Postgres service.` : null,
846
+ Review generated SQL before staging or applying it. \`DATABASE_URL\` points to a managed Neon branch or project; there is intentionally no local Postgres service.` : null,
836
847
  options.features.auth ? `### Use authentication
837
848
 
838
849
  Frontend applications use their generated Clerk provider and hooks. The API verifies Clerk sessions. Public keys may use a framework public environment prefix; \`CLERK_SECRET_KEY\` remains in the API environment only.` : null,
@@ -840,57 +851,22 @@ Frontend applications use their generated Clerk provider and hooks. The API veri
840
851
 
841
852
  Use \`@shared/api-client\`. The API authorizes the user and creates a short-lived signed R2 upload; the client uploads directly and confirms through the API. Never put R2 credentials in a client or make the bucket public.` : null,
842
853
  ].filter((value) => value !== null).join('\n\n');
843
- const verticalSliceExample = options.apps.api && options.features.database ? `## Copy-paste example: a projects feature
844
-
845
- These files are user-owned extension seams. Anhedral preserves them during \`add\`, \`ui add\`, and \`upgrade\`.
846
-
847
- \`packages/contracts/src/app.ts\`:
848
-
849
- \`\`\`ts
850
- import { z } from 'zod';
851
-
852
- export const ProjectSchema = z.object({ id: z.string(), name: z.string() });
853
- export const ProjectListSchema = z.array(ProjectSchema);
854
- \`\`\`
855
-
856
- \`packages/db/src/app-schema.ts\`:
854
+ const verticalSliceExample = options.apps.api && options.features.database ? `## Your working starter feature
857
855
 
858
- \`\`\`ts
859
- import { pgTable, text } from 'drizzle-orm/pg-core';
860
-
861
- export const projects = pgTable('projects', {
862
- id: text('id').primaryKey(),
863
- name: text('name').notNull(),
864
- });
865
- \`\`\`
866
-
867
- \`apps/api/src/routes/app.ts\`:
868
-
869
- \`\`\`ts
870
- import type { FastifyPluginAsync } from 'fastify';
871
- import { ProjectListSchema } from '@shared/contracts';
872
- import { db } from '@shared/db';
873
- import { projects } from '@shared/db/schema';
874
-
875
- export const appRoutes: FastifyPluginAsync = async (app) => {
876
- app.get('/projects', async () => ProjectListSchema.parse(await db.select().from(projects)));
877
- };
878
- \`\`\`
856
+ Anhedral generated an editable **items** feature all the way through the stack:
879
857
 
880
- ${options.apps.web || options.apps.mobile || options.apps.desktop || options.apps.extension ? `\`packages/api-client/src/app.ts\`:
858
+ - \`packages/db/src/app-schema.ts\` owns the table.
859
+ - \`packages/contracts/src/app.ts\` owns the request and response schemas.
860
+ - \`apps/api/src/routes/app.ts\` owns the list and create endpoints.
861
+ ${options.apps.web || options.apps.mobile || options.apps.desktop || options.apps.extension ? '- `packages/api-client/src/app.ts` owns the typed client calls.' : ''}
862
+ ${options.apps.web ? '- `apps/web/components/item-list.tsx` is a working UI for the feature.' : ''}
863
+ ${options.apps.mobile ? '- `apps/mobile/components/item-list.tsx` is the native UI for the feature.' : ''}
864
+ ${options.apps.desktop ? '- `apps/desktop/src/renderer/components/item-list.tsx` is the desktop UI for the feature.' : ''}
865
+ ${options.apps.extension ? '- `apps/extension/src/components/item-list.tsx` is the extension UI for the feature.' : ''}
881
866
 
882
- \`\`\`ts
883
- import { ProjectListSchema } from '@shared/contracts';
884
- import { ApiClient } from './generated';
867
+ These are user-owned extension seams. Rename or replace the example with your product model; Anhedral preserves your changes during \`add\`, \`ui add\`, and \`upgrade\`.
885
868
 
886
- export function listProjects(client: ApiClient) {
887
- return client.request('/projects', { method: 'GET' }, ProjectListSchema);
888
- }
889
- \`\`\`
890
-
891
- Import \`listProjects\` and the generated client factory in the frontend surface you are building.` : ''}
892
-
893
- Then run \`pnpm db:generate\`, review and commit the migration, run \`pnpm verify\`, apply it to the intended Neon branch with \`pnpm db:migrate\`, and start the relevant app surfaces.` : '';
869
+ To run it, set \`DATABASE_URL\`, run \`pnpm ready\`, generate and review the migration, stage it with \`git add packages/db/migrations\`, run \`pnpm verify\`, apply it with \`pnpm db:migrate\`, and start the app with \`pnpm dev\`.` : '';
894
870
  const dependencyLines = [
895
871
  options.apps.api ? 'frontend apps -> @shared/api-client -> HTTP -> apps/api' : null,
896
872
  options.apps.api ? 'frontend apps -> @shared/contracts <- apps/api' : null,
@@ -901,8 +877,8 @@ Then run \`pnpm db:generate\`, review and commit the migration, run \`pnpm verif
901
877
  ? `### Add UI primitives
902
878
 
903
879
  \`\`\`sh
904
- anhedral ui add dialog
905
- ${options.apps.web ? 'anhedral ui add data-table --target web\n' : ''}\`\`\`
880
+ pnpm anhedral:ui dialog
881
+ ${options.apps.web ? 'pnpm anhedral:ui data-table --target web\n' : ''}\`\`\`
906
882
 
907
883
  DOM apps receive source-owned shadcn/ui files. Mobile receives React Native Reusables files. Customize those files normally.`
908
884
  : '';
@@ -919,13 +895,24 @@ Anhedral installs workspace dependencies during generation unless you used \`--s
919
895
  ${environmentSetupCommands}
920
896
  \`\`\`
921
897
 
922
- Stop here and replace every required placeholder in those uncommitted files. In particular, \`DATABASE_URL\` must be the pooled connection string for the intended managed Neon branch when the database module is selected. Then generate the initial migration and verify the workspace:
898
+ Stop here and replace every required placeholder in those uncommitted files.${options.features.database ? ' `DATABASE_URL` must be the pooled connection string for the intended managed Neon branch.' : ''} Then verify the workspace:
923
899
 
924
900
  \`\`\`sh
925
901
  ${firstVerificationCommands}
926
902
  \`\`\`
927
903
 
928
- After verification succeeds, run \`pnpm dev\`. In a full stack this starts the primary web + API development loop, so opening the project does not also launch Expo, Electron, and WXT. Use \`pnpm dev:all\` when you intentionally want every selected surface, or run one surface:
904
+ After verification succeeds, run \`pnpm dev\`. It starts ${options.apps.web && options.apps.api
905
+ ? 'the primary web + API loop'
906
+ : options.apps.web ? 'the web application'
907
+ : options.apps.mobile && options.apps.api ? 'the mobile + API loop'
908
+ : options.apps.mobile ? 'the mobile application'
909
+ : options.apps.desktop && options.apps.api ? 'the desktop + API loop'
910
+ : options.apps.desktop ? 'the desktop application'
911
+ : options.apps.extension && options.apps.api ? 'the extension + API loop'
912
+ : options.apps.extension ? 'the extension'
913
+ : 'the API'}${Object.values(options.apps).filter(Boolean).length > (options.apps.api && Object.values(options.apps).some((selected, index) => selected && index !== 2) ? 2 : 1)
914
+ ? ' without launching every additional surface. Use `pnpm dev:all` when you intentionally want all selected surfaces'
915
+ : ''}, or run one surface:
929
916
 
930
917
  ${moduleCommands}
931
918
 
@@ -945,11 +932,11 @@ ${modules.map((moduleName) => `- \`${moduleName}\``).join('\n')}
945
932
 
946
933
  ## UI and customization
947
934
 
948
- DOM clients use source-owned shadcn/ui. Expo uses React Native Reusables with ${options.nativeStyling}. Add components with \`anhedral ui add <component>\`; use \`--target\` when a component belongs to one client. Customize the resulting source normally.
935
+ ${options.apps.web || options.apps.desktop || options.apps.extension ? 'DOM clients use source-owned shadcn/ui.' : ''}${options.apps.mobile ? `${options.apps.web || options.apps.desktop || options.apps.extension ? ' ' : ''}Expo uses React Native Reusables with ${options.nativeStyling}.` : ''} Add components with \`pnpm anhedral:ui <component>\`; use \`--target\` when a component belongs to one client. Customize the resulting source normally.
949
936
 
950
937
  ## Environment and first verification
951
938
 
952
- The setup blocks above are the first-run checklist. Provider secrets stay in their package-local environment files; none belong in client code. This project uses managed Neon and intentionally does not generate or start local Postgres.
939
+ The setup blocks above are the first-run checklist. Provider secrets stay in their package-local environment files; none belong in client code.${options.features.database ? ' This project uses managed Neon and intentionally does not generate or start local Postgres.' : ''}
953
940
 
954
941
  ## Deployment
955
942
 
@@ -963,7 +950,7 @@ ${deploymentRows}
963
950
  ${deploymentCommands}
964
951
  \`\`\`
965
952
 
966
- Read \`PRODUCTION.md\` before creating accounts or production resources. Generated ownership and tool versions are recorded in \`anhedral.json\`. Run \`anhedral doctor\` before structural changes and preview them with \`--dry-run\`.
953
+ Read \`PRODUCTION.md\` before creating accounts or production resources. Generated ownership and tool versions are recorded in \`anhedral.json\`. Run \`pnpm anhedral:doctor\` before structural changes and preview additions with \`pnpm anhedral:add <module> --dry-run\`.
967
954
  `);
968
955
  }
969
956
  writeFile(path.join(root, 'docs/DEVELOPMENT.md'), `# Developing ${markdownHeading(options.displayName)}
@@ -985,17 +972,17 @@ ${uiTaskSection}
985
972
  ### Add another app surface
986
973
 
987
974
  \`\`\`sh
988
- anhedral add mobile --dry-run
989
- anhedral add mobile
975
+ pnpm anhedral:add mobile --dry-run
976
+ pnpm anhedral:add mobile
990
977
  \`\`\`
991
978
 
992
- Anhedral refuses ownership conflicts instead of overwriting product changes. Run \`anhedral doctor\` when an add cannot proceed.
979
+ Anhedral refuses ownership conflicts instead of overwriting product changes. Run \`pnpm anhedral:doctor\` when an add cannot proceed.
993
980
 
994
981
  ## Verification
995
982
 
996
983
  - While iterating, run the affected package's typecheck or tests.
997
984
  - Before handoff, run \`pnpm verify\`.
998
- - Before generation, run \`anhedral doctor\` and use \`--dry-run\`.
985
+ - Before generation, run \`pnpm anhedral:doctor\` and use \`--dry-run\`.
999
986
  - For schema changes, commit reviewed migration SQL and run \`pnpm verify:db\`.
1000
987
  `);
1001
988
  writeFile(path.join(root, 'docs/STACK.md'), `# Stack guide
@@ -1014,7 +1001,7 @@ ${dependencyLines || 'selected applications are independent framework projects'}
1014
1001
 
1015
1002
  Clients may import contracts and the API client. They must not import API services, database connections, or server environment modules. Provider secrets terminate at the API or provider-specific Worker.
1016
1003
 
1017
- \`anhedral add\` adds modules and integration files. \`anhedral ui add\` adds source-owned UI. \`anhedral doctor\` checks recorded ownership. Product features, models, pages, routes, and services remain developer-owned TypeScript.
1004
+ \`pnpm anhedral:add <module>\` adds modules and integration files. \`pnpm anhedral:ui <component>\` adds source-owned UI. \`pnpm anhedral:doctor\` checks recorded ownership. Product features, models, pages, routes, and services remain developer-owned TypeScript.
1018
1005
  `);
1019
1006
  if (includeUserDocs) {
1020
1007
  const productionItems = [
@@ -1032,27 +1019,28 @@ Clients may import contracts and the API client. They must not import API servic
1032
1019
  options.features.database ? '- Commit every reviewed Drizzle SQL migration and its metadata with the schema change; `pnpm verify:db` rejects a missing or untracked SQL baseline and validates migration history.' : null,
1033
1020
  options.features.database ? '- Generated CI runs `pnpm db:generate` and fails when `packages/db/migrations` changes, preventing schema changes without a matching migration.' : null,
1034
1021
  '- Run `pnpm verify` before deployment.',
1035
- '- Run `anhedral doctor` before incremental generation.',
1022
+ '- Run `pnpm anhedral:doctor` before incremental generation.',
1036
1023
  ].filter((item) => item !== null);
1037
1024
  const productionSections = [
1038
- options.apps.web || options.apps.api || options.features.storage ? `## Domain registration and Cloudflare control plane
1025
+ options.features.storage || options.features.electronUpdater ? `## Domain and Cloudflare control plane
1039
1026
 
1040
- Registration and DNS hosting are separate jobs. You can buy the domain at GoDaddy while Cloudflare becomes authoritative DNS immediately; transferring the registrar later is optional.
1027
+ Use an organization-owned custom domain and registrar of your choice. Registration, authoritative DNS, and application hosting are separate decisions; transferring the registrar is not required.
1041
1028
 
1042
- 1. Buy the domain at GoDaddy under an organization-controlled account. Enable auto-renewal, MFA, accurate registrant contact details, and record renewal ownership.
1043
- 2. Add the domain as a website/zone in Cloudflare. Before changing nameservers, copy every existing DNS record—especially MX, TXT, DKIM, SPF, DMARC, verification, and subdomain records—and disable DNSSEC/DS records at GoDaddy if enabled.
1044
- 3. Cloudflare assigns two authoritative nameservers. Replace the GoDaddy nameservers with those exact values, then wait until Cloudflare marks the zone **Active**. Recreate and verify mail and other records before considering the migration complete. Re-enable DNSSEC in Cloudflare after activation.
1045
- 4. Optional registrar transfer: after the domain is eligible (commonly not within 60 days of registration, a prior transfer, or certain registrant changes), unlock it at GoDaddy, request its EPP/authorization code, and start **Transfer Domains** in Cloudflare. Approve the transfer and payment. DNS can remain live throughout because Cloudflare was already authoritative.
1046
- 5. Keep Cloudflare as the shared domain control plane: DNS and DNSSEC at the zone; Workers and R2 for the asset hostname; and optional Queues, Workflows, Durable Objects, WAF, Turnstile, or other compute/security products when the application actually needs them. Vercel remains the web/API runtime.
1029
+ 1. Add the domain to an organization-owned Cloudflare account because the selected R2 Worker custom domains require an active Cloudflare zone.
1030
+ 2. Before changing authoritative nameservers, copy every existing DNS record—especially MX, TXT, DKIM, SPF, DMARC, verification, and subdomain records. Follow the current registrar's instructions and preserve mail delivery.
1031
+ 3. Replace the registrar's nameservers with the exact pair Cloudflare assigns, wait for the zone to become **Active**, then enable DNSSEC when supported.
1032
+ 4. Keep Vercel application records DNS-only. Worker custom domains such as \`assets.<domain>\` and \`updates.<domain>\` are created by Wrangler and remain proxied.
1047
1033
 
1048
- Use organization-owned Cloudflare and GoDaddy accounts with MFA and least-privilege member roles. Do not confuse changing nameservers (DNS delegation) with transferring the registration (registrar/billing ownership). Follow the current [GoDaddy nameserver instructions](https://help.dc-aws.godaddy.com/help/edit-my-domain-nameservers-664), [GoDaddy transfer-away sequence](https://www.godaddy.com/en-ca/help/transfer-my-domain-away-from-godaddy-3560), [Cloudflare nameserver guidance](https://developers.cloudflare.com/dns/nameservers/update-nameservers/), and [Cloudflare registrar-transfer sequence](https://developers.cloudflare.com/registrar/get-started/transfer-domain-to-cloudflare/).` : null,
1034
+ Use MFA and least-privilege roles. Record who owns registration renewal, DNS, provider billing, and recovery access. See [Cloudflare nameserver guidance](https://developers.cloudflare.com/dns/nameservers/update-nameservers/).` : options.apps.web || options.apps.api ? `## Custom domain (optional)
1035
+
1036
+ You can deploy previews and production on provider-generated domains before buying or connecting a custom domain. If you add one later, keep the registrar and DNS provider you prefer, add the domain in Vercel first, and create only the exact verification and routing records Vercel reports. Use organization-owned accounts, MFA, auto-renewal, and documented recovery ownership.` : null,
1049
1037
  options.apps.web || options.apps.api ? `## Vercel: web and API
1050
1038
 
1051
1039
  1. Push the repository to GitHub and import it in Vercel. Choose the **Services** framework preset and keep the repository root as the project root; \`vercel.json\` maps the selected \`apps/web\` and \`apps/api\` services.
1052
1040
  2. Add every required variable from the package-local \`.env.example\` files to Vercel's Development, Preview, and Production environments. Server secrets belong only to the API service. Never put a secret in a \`NEXT_PUBLIC_*\` variable.
1053
- 3. Run \`pnpm deploy:vercel:link\` for local CLI access. The normal release path is Git: every non-production branch/PR receives a preview deployment, and merging to the configured production branch creates production automatically. The explicit \`deploy:vercel:preview\` and \`deploy:vercel:production\` scripts are escape hatches for manual releases.
1054
- 4. Run \`pnpm deploy:vercel:inspect -- <deployment-url>\` when diagnosing a deployment. Verify health, authentication, scheduled jobs, and provider callbacks in Preview before merging.
1055
- 5. Add \`app.example.com\` in Vercel **Project → Settings → Domains** before changing DNS. Inspect it with \`pnpm deploy:vercel:domain:inspect -- app.example.com\`, then create the exact A/CNAME/TXT records Vercel reports in Cloudflare DNS. Leave the Vercel A/CNAME record **DNS only** (gray cloud), because placing Cloudflare's reverse proxy in front of Vercel hides traffic signals, adds another CDN hop, and can interfere with Vercel caching/firewall behavior. Ownership-verification TXT records are also DNS only. Vercel provisions TLS after validation.
1041
+ 3. Run \`pnpm deploy:vercel:link\` for local CLI access. The normal release path is Git: every non-production branch/PR receives a preview deployment, and merging to the configured production branch creates production automatically. Use \`deploy:vercel:preview\` or \`deploy:vercel:production\` when you intentionally need a manual release.
1042
+ 4. Run \`pnpm deploy:vercel:inspect <deployment-url>\` when diagnosing a deployment. Verify health, authentication, scheduled jobs, and provider callbacks in Preview before merging.
1043
+ 5. If you use a custom domain, add \`app.example.com\` in Vercel **Project → Settings → Domains** before changing DNS. Inspect it with \`pnpm deploy:vercel:domain:inspect app.example.com\`, then create the exact A/CNAME/TXT records Vercel reports at your DNS provider. When Cloudflare is authoritative, leave Vercel A/CNAME and verification records DNS-only. Vercel provisions TLS after validation.
1056
1044
 
1057
1045
  When web and API are both selected, keep the generated \`/api/(.*)\` route before the web catch-all. The browser uses same-origin \`/api\`; mobile, desktop, and extension builds need the absolute production API URL.` : null,
1058
1046
  options.features.auth ? `## Clerk: production identity
@@ -1063,7 +1051,7 @@ When web and API are both selected, keep the generated \`/api/(.*)\` route befor
1063
1051
  4. Redeploy every selected client after changing public Clerk keys. Test sign-in, sign-out, token refresh, deep links, and physical-device behavior before release.${options.apps.extension ? '\n5. For the extension, create a stable CRX ID, configure Clerk Chrome Extension deployment for that ID, and set `VITE_CLERK_FRONTEND_API_URL` plus `VITE_CLERK_SYNC_HOST` when using web-to-extension session sync. OAuth and email-link flows require Sync Host.' : ''}` : null,
1064
1052
  options.features.database ? `## Neon and Drizzle: database
1065
1053
 
1066
- 1. Create a Neon account, then run \`pnpm neon:login\` and \`pnpm neon:project:create -- --name ${options.projectName}\`, or create the project in the Neon console.
1054
+ 1. Create a Neon account, then run \`pnpm neon:login\` and \`pnpm neon:project:create --name ${options.projectName}\`, or create the project in the Neon console.
1067
1055
  2. Copy the pooled Postgres connection string to \`DATABASE_URL\` in \`packages/db/.env\` locally and the API's Vercel environment in production. Keep preview and production databases isolated with Neon branches or separate projects.
1068
1056
  3. Change the Drizzle schema, run \`pnpm db:generate\`, review and commit the SQL and metadata, then run \`pnpm verify:db\`.
1069
1057
  4. Apply \`pnpm db:migrate\` against the intended database as a controlled release step before sending production traffic to code that requires the new schema. Backward-compatible migrations make rollback safer; never run unreviewed migration generation during a Vercel build.
@@ -1103,7 +1091,7 @@ The generated update path is \`electron-updater -> updates.<domain> Worker -> pr
1103
1091
  1. In \`apps/desktop-updater-worker/wrangler.jsonc\`, replace both \`updates.example.com\` values with a hostname in an active Cloudflare zone. Run \`pnpm desktop:updates:cloudflare:login\`, then run \`pnpm desktop:updates:first-provision\` exactly once to create the private bucket and deploy the bound Worker, managed DNS record, TLS certificate, logs, and custom domain. For repeat deployments use \`pnpm desktop:updates:worker:deploy\`; bucket creation is intentionally not repeated. Keep the bucket's \`r2.dev\` URL and R2 bucket custom-domain access disabled.
1104
1092
  2. Copy \`apps/desktop/electron-builder.env.example\` to \`apps/desktop/electron-builder.env\` and set \`DESKTOP_UPDATE_BASE_URL\` to that exact HTTPS origin with no trailing slash. electron-builder writes the platform/architecture URL into the packaged app's update configuration; no update secret is embedded.
1105
1093
  3. Increase \`apps/desktop/package.json\`'s version for every release. Build on each target OS using \`pnpm desktop:updates:build:mac\`, \`pnpm desktop:updates:build:win\`, or \`pnpm desktop:updates:build:linux\`. Configure macOS signing/notarization and Windows code-signing credentials in CI, never in the repository.
1106
- 4. From the repository root, upload one architecture at a time, for example \`pnpm desktop:updates:publish -- --platform mac --arch arm64\`. The publisher accepts only known platform/architecture values, uploads immutable installers and blockmaps first, then uploads mutable \`latest*.yml\` metadata last. Run it only after signing succeeds.
1094
+ 4. From the repository root, upload one architecture at a time, for example \`pnpm desktop:updates:publish --platform mac --arch arm64\`. The publisher accepts only known platform/architecture values, uploads immutable installers and blockmaps first, then uploads mutable \`latest*.yml\` metadata last. Run it only after signing succeeds.
1107
1095
  5. Install the previous signed version, publish a newer version, and verify update discovery, range downloads, signature validation, restart/install, rollback behavior, and logs on macOS, Windows, and the supported Linux package path. Test clean install, deep links, Clerk return flow, and OS security warnings too.
1108
1096
 
1109
1097
  The Worker accepts GET/HEAD only, validates the configured hostname and \`releases/\` prefix, supports conditionals and single-range downloads, serves metadata with \`no-store\`, and serves versioned artifacts as immutable. See \`cloudflare/desktop-updates.md\` for the generated resource contract.` : options.apps.desktop ? `## Electron desktop
@@ -1122,7 +1110,7 @@ ${productionSections.join('\n\n')}
1122
1110
 
1123
1111
  ## Final release order
1124
1112
 
1125
- - Run \`pnpm verify\` and \`anhedral doctor\`; review the exact production diff.
1113
+ - Run \`pnpm verify\` and \`pnpm anhedral:doctor\`; review the exact production diff.
1126
1114
  ${options.features.database ? '- Apply reviewed, backward-compatible database migrations.' : ''}
1127
1115
  ${options.apps.web || options.apps.api ? '- Deploy provider infrastructure and secrets, then deploy API/web.' : ''}
1128
1116
  ${options.apps.mobile || options.apps.extension || options.apps.desktop ? '- Build immutable client artifacts only after production URLs and public keys are final.\n- Release to internal testers, verify telemetry and rollback, then promote through each store review/rollout.' : ''}
@@ -1134,11 +1122,11 @@ Generator: ${GENERATOR_VERSION}
1134
1122
 
1135
1123
  Resolved modules: ${modules.join(', ')}
1136
1124
 
1137
- - \`anhedral add <module> --dry-run\` previews incremental changes.
1138
- - \`anhedral upgrade --dry-run\` previews a supported generator migration.
1139
- - \`anhedral ui add <component> --dry-run\` previews platform-routed component additions.
1140
- - \`anhedral doctor\` reports manifest and filesystem drift before incremental changes.
1141
- - README and existing user workflows are never rewritten by \`anhedral add\`.
1125
+ - \`pnpm anhedral:add <module> --dry-run\` previews incremental changes.
1126
+ - \`pnpm anhedral:upgrade --dry-run\` previews a supported generator migration.
1127
+ - \`pnpm anhedral:ui <component> --dry-run\` previews platform-routed component additions.
1128
+ - \`pnpm anhedral:doctor\` reports manifest and filesystem drift before incremental changes.
1129
+ - README and existing user workflows are never rewritten by Anhedral lifecycle commands.
1142
1130
  `);
1143
1131
  const surfaceGuidance = [
1144
1132
  options.apps.web ? '- Web lives in `apps/web`: use Next.js App Router, server-first rendering, and shadcn/ui source components.' : null,
@@ -1184,9 +1172,10 @@ Before implementing a feature, read \`README.md\` for the source map, \`docs/DEV
1184
1172
  ## Start safely
1185
1173
 
1186
1174
  1. Read \`anhedral.json\` for the selected modules, native styling provider, installed UI components, and file ownership.
1187
- 2. Run \`pnpm dlx anhedral@${GENERATOR_VERSION} doctor\` before generator operations. If it reports a supported older generator, preview and apply \`anhedral upgrade\` before continuing.
1188
- 3. Preview structural changes with \`anhedral upgrade --dry-run\`, \`anhedral add <module> --dry-run\`, or \`anhedral ui add <component> --dry-run\` as applicable.
1189
- 4. Never hand-edit \`anhedral.json\`, ownership hashes, or bundled-template provenance.
1175
+ 2. Run \`pnpm first-run\` on a fresh clone, then run \`pnpm ready\`. The readiness check reports only missing filenames and variable names; never print populated environment files.
1176
+ 3. Run \`pnpm anhedral:doctor\` before generator operations. If it reports a supported older generator, preview and apply \`pnpm anhedral:upgrade --dry-run\` before continuing.
1177
+ 4. Preview structural changes with \`pnpm anhedral:upgrade --dry-run\`, \`pnpm anhedral:add <module> --dry-run\`, or \`pnpm anhedral:ui <component> --dry-run\` as applicable.
1178
+ 5. Never hand-edit \`anhedral.json\`, ownership hashes, or bundled-template provenance.
1190
1179
 
1191
1180
  Resolved modules: ${modules.map((moduleName) => `\`${moduleName}\``).join(', ')}.
1192
1181
 
@@ -1203,7 +1192,7 @@ ${skillFeatureSteps}
1203
1192
 
1204
1193
  ## UI conventions
1205
1194
 
1206
- - Add source-owned components with \`pnpm dlx anhedral@${GENERATOR_VERSION} ui add <component>\`; use \`--target\` only when the component should not exist in every selected client.
1195
+ - Add source-owned components with \`pnpm anhedral:ui <component>\`; use \`--target\` only when the component should not exist in every selected client.
1207
1196
  - Web, desktop, and extension components come from shadcn/ui. Mobile components come from React Native Reusables using ${options.nativeStyling}.
1208
1197
  - Compose application-specific UI around installed primitives. Preserve accessibility, keyboard behavior, focus handling, and platform conventions.
1209
1198
  - Use lowercase kebab-case filenames, PascalCase component exports, and \`use-\` prefixes for hooks.
@@ -1211,7 +1200,7 @@ ${skillFeatureSteps}
1211
1200
  ## Ownership rules
1212
1201
 
1213
1202
  - \`README.md\`, \`PRODUCTION.md\`, product UI, and the backend \`app.ts\`/\`app-schema.ts\` extension seams are user-owned. Root JSON/YAML configuration is mergeable. Other recorded files are generator-managed unless \`anhedral.json\` says otherwise.
1214
- - Prefer new application files and narrow composition points over editing managed substrate files. If a managed file must change, understand that future \`anhedral add\` operations will stop until the project is reconciled.
1203
+ - Prefer new application files and narrow composition points over editing managed substrate files. If a managed file must change, understand that future \`pnpm anhedral:add\` operations will stop until the project is reconciled.
1215
1204
  - Never overwrite user changes, bypass a generator conflict, or replace a failed ownership check with fabricated hashes.
1216
1205
 
1217
1206
  ## Environment and security
@@ -1232,7 +1221,11 @@ When the user asks you to provision this project:
1232
1221
  4. At every secret-generation screen, stop before the final button. Tell the user the exact button, variable name, and uncommitted package-local environment file. The user clicks and pastes the value directly into that file or the provider's protected field, never into chat. Validate presence without printing populated environment files.
1233
1222
  5. Read \`PRODUCTION.md\` completely and follow its selection-specific order. Do not claim completion until cloud resources, environment names, DNS, TLS, application health, and the verification commands below pass.
1234
1223
 
1235
- Domain topology: Cloudflare is authoritative DNS; Vercel hosts web/API; the Vercel A/CNAME/TXT records are created in Cloudflare DNS and normally remain DNS-only. Generated Worker hostnames such as \`assets.<domain>\` or \`updates.<domain>\` are Cloudflare Worker Custom Domains created by Wrangler, not CNAMEs to Vercel or public R2 endpoints.
1224
+ ${options.features.storage || options.features.electronUpdater
1225
+ ? 'Domain topology: Cloudflare is authoritative DNS for the selected Worker custom domains. Vercel hosts web/API and its records remain DNS-only. Generated Worker hostnames such as `assets.<domain>` or `updates.<domain>` are created by Wrangler, not CNAMEs to Vercel or public R2 endpoints.'
1226
+ : options.apps.web || options.apps.api
1227
+ ? 'Domain topology: a custom domain is optional. Vercel provides preview and production domains; if the user connects a custom domain, use their chosen registrar and DNS provider and create only the records Vercel reports.'
1228
+ : 'Domain topology depends on the selected platform release channels; do not introduce a DNS provider unless the product actually needs a custom domain.'}
1236
1229
 
1237
1230
  - Read \`PRODUCTION.md\` before provisioning or changing production resources. It is tailored to this project's selected surfaces and providers.
1238
1231
  ${options.apps.web || options.apps.api ? '- Prefer GitHub-triggered Vercel Preview and Production deployments. Keep the generated Services routing intact; use manual `deploy:vercel:*` scripts only when explicitly required.' : ''}
@@ -1248,9 +1241,10 @@ Run the strongest applicable checks before handing work back:
1248
1241
 
1249
1242
  \`\`\`sh
1250
1243
  pnpm typecheck
1244
+ pnpm ready
1251
1245
  pnpm verify
1252
1246
  pnpm build
1253
- pnpm dlx anhedral@${GENERATOR_VERSION} doctor
1247
+ pnpm anhedral:doctor
1254
1248
  \`\`\`
1255
1249
 
1256
1250
  Use the package-specific \`verify:*\` scripts while iterating. A database schema change is incomplete until its reviewed migration is Git-tracked. Report exact failing packages and commands; do not weaken checks to make a change pass.
@@ -1400,7 +1394,7 @@ function readProjectManifest(root, options = {}) {
1400
1394
  return manifest;
1401
1395
  }
1402
1396
  const nextStep = isSupportedProjectUpgrade(manifest.generatorVersion, GENERATOR_VERSION)
1403
- ? 'Run anhedral upgrade before adding modules.'
1397
+ ? 'Run `pnpm anhedral:upgrade` before adding modules.'
1404
1398
  : 'Regenerate the project with the current CLI before adding modules.';
1405
1399
  throw new Error(`This project was generated by Anhedral ${manifest.generatorVersion}; current CLI ${GENERATOR_VERSION} only supports exact-current projects. `
1406
1400
  + nextStep);
@@ -1424,15 +1418,66 @@ function cleanNestedArtifacts(root) {
1424
1418
  function selectedUiTargets(options) {
1425
1419
  return UI_TARGETS.filter((target) => options.apps[target]);
1426
1420
  }
1421
+ function bundledUiComponents(options) {
1422
+ const entries = [];
1423
+ const add = (target, name) => {
1424
+ entries.push(Object.freeze({
1425
+ name,
1426
+ target,
1427
+ provider: 'shadcn',
1428
+ source: `anhedral:bundled/${name}`,
1429
+ variant: null,
1430
+ }));
1431
+ };
1432
+ if (options.apps.web) {
1433
+ add('web', 'button');
1434
+ add('web', 'card');
1435
+ }
1436
+ if (options.apps.desktop)
1437
+ add('desktop', 'button');
1438
+ if (options.apps.extension)
1439
+ add('extension', 'button');
1440
+ return Object.freeze(entries);
1441
+ }
1427
1442
  function installSelectedUiComponents(root, options) {
1428
1443
  const installs = resolveUiInstalls(options.uiComponents, selectedUiTargets(options), options.nativeStyling);
1429
1444
  if (installs.length > 0 && !options.dryRun)
1430
1445
  installUiComponents(root, installs);
1431
- return installs;
1446
+ return mergeUiInstalls(bundledUiComponents(options), installs);
1432
1447
  }
1433
1448
  function installWorkspaceDependencies(root) {
1434
1449
  const executable = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
1450
+ anhedralPrint.step('Installing workspace dependencies');
1435
1451
  execFile(executable, ['install', '--no-frozen-lockfile'], root);
1452
+ anhedralPrint.done('Workspace dependencies installed');
1453
+ }
1454
+ function initializeGitRepository(root) {
1455
+ const insideWorktree = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
1456
+ cwd: root,
1457
+ encoding: 'utf8',
1458
+ stdio: ['ignore', 'pipe', 'ignore'],
1459
+ });
1460
+ if (insideWorktree.status === 0) {
1461
+ anhedralPrint.info('Using the existing Git worktree.');
1462
+ return;
1463
+ }
1464
+ if (insideWorktree.error?.code === 'ENOENT') {
1465
+ anhedralPrint.info('Git is not installed; skipped repository initialization.');
1466
+ return;
1467
+ }
1468
+ const initialized = spawnSync('git', ['init'], {
1469
+ cwd: root,
1470
+ encoding: 'utf8',
1471
+ stdio: ['ignore', 'pipe', 'pipe'],
1472
+ });
1473
+ if (initialized.error?.code === 'ENOENT') {
1474
+ anhedralPrint.info('Git is not installed; skipped repository initialization.');
1475
+ return;
1476
+ }
1477
+ if (initialized.status !== 0) {
1478
+ throw new Error(`Unable to initialize Git: ${String(initialized.stderr).trim() || 'git init failed'}`);
1479
+ }
1480
+ anhedralPrint.done('Git repository initialized');
1436
1481
  }
1437
1482
  async function writeSelectedModules(root, options) {
1438
1483
  const templates = materializeTemplates(root, resolveModules(options.modules).resolvedModules);
@@ -1454,14 +1499,56 @@ async function writeSelectedModules(root, options) {
1454
1499
  scaffoldElectronUpdater(root, shared);
1455
1500
  return templates;
1456
1501
  }
1457
- function printPlan(operation, paths, json) {
1502
+ function printPlan(operation, paths, json, modules, context = {
1503
+ dryRun: false,
1504
+ skipInstall: false,
1505
+ rootDirectory: process.cwd(),
1506
+ }) {
1507
+ const nextSteps = context.dryRun
1508
+ ? ['Review the plan, then re-run the same command without --dry-run.']
1509
+ : [
1510
+ context.skipInstall ? 'pnpm install' : null,
1511
+ 'pnpm first-run',
1512
+ 'pnpm ready',
1513
+ operation === 'init' ? null : 'pnpm verify',
1514
+ ].filter((value) => value !== null);
1458
1515
  if (json) {
1459
- console.log(JSON.stringify({ operation, paths }, null, 2));
1516
+ console.log(JSON.stringify({
1517
+ operation,
1518
+ status: context.dryRun ? 'planned' : 'applied',
1519
+ generatorVersion: GENERATOR_VERSION,
1520
+ rootDirectory: context.rootDirectory,
1521
+ ...(modules ? {
1522
+ requestedModules: modules.requestedModules,
1523
+ resolvedModules: modules.resolvedModules,
1524
+ dependencyAddedModules: modules.dependencyAddedModules,
1525
+ dependencyEdges: modules.dependencyEdges,
1526
+ } : {}),
1527
+ paths,
1528
+ nextSteps,
1529
+ }, null, 2));
1460
1530
  return;
1461
1531
  }
1532
+ if (modules) {
1533
+ anhedralPrint.info(`requested modules: ${modules.requestedModules.join(', ')}`);
1534
+ if (modules.dependencyAddedModules.length > 0) {
1535
+ anhedralPrint.info(`added by dependencies: ${modules.dependencyAddedModules.join(', ')}`);
1536
+ }
1537
+ anhedralPrint.info(`resolved modules: ${modules.resolvedModules.join(', ')}`);
1538
+ }
1462
1539
  anhedralPrint.info(`${operation} plan: ${paths.length} path${paths.length === 1 ? '' : 's'}`);
1463
- for (const relativePath of paths)
1464
- console.log(` ${relativePath}`);
1540
+ if (context.dryRun || process.env.ANHEDRAL_VERBOSE === '1') {
1541
+ for (const relativePath of paths)
1542
+ console.log(` ${relativePath}`);
1543
+ }
1544
+ else {
1545
+ anhedralPrint.info('Use --dry-run or --verbose to inspect every changed path.');
1546
+ }
1547
+ if (nextSteps.length > 0) {
1548
+ console.log('\nNext:');
1549
+ for (const step of nextSteps)
1550
+ console.log(` ${step}`);
1551
+ }
1465
1552
  }
1466
1553
  function assertSafeChangedPaths(root, stageRoot, manifest, paths, deleting = new Set()) {
1467
1554
  for (const relativePath of paths) {
@@ -1543,6 +1630,10 @@ export async function scaffoldProject(inputOptions) {
1543
1630
  commitPaths.push(...readdirSync(stageRoot));
1544
1631
  },
1545
1632
  afterCommit: () => {
1633
+ if (options.initializeGit) {
1634
+ anhedralPrint.section('Version control');
1635
+ initializeGitRepository(root);
1636
+ }
1546
1637
  if (!options.skipInstall) {
1547
1638
  anhedralPrint.section('Workspace install');
1548
1639
  installWorkspaceDependencies(root);
@@ -1550,11 +1641,19 @@ export async function scaffoldProject(inputOptions) {
1550
1641
  },
1551
1642
  });
1552
1643
  if (options.dryRun)
1553
- printPlan('init', planned, options.json);
1644
+ printPlan('init', planned, options.json, resolveModules(options.modules), {
1645
+ dryRun: true,
1646
+ skipInstall: options.skipInstall,
1647
+ rootDirectory: root,
1648
+ });
1554
1649
  if (!options.dryRun)
1555
1650
  anhedralPrint.done(`Committed ${planned.length} paths`);
1556
1651
  if (!options.dryRun && options.json)
1557
- printPlan('init', planned, true);
1652
+ printPlan('init', planned, true, resolveModules(options.modules), {
1653
+ dryRun: false,
1654
+ skipInstall: options.skipInstall,
1655
+ rootDirectory: root,
1656
+ });
1558
1657
  }
1559
1658
  finally {
1560
1659
  if (previousChannel == null)
@@ -1638,7 +1737,17 @@ export async function scaffoldUiComponents(uiOptions) {
1638
1737
  });
1639
1738
  if (noOp) {
1640
1739
  if (uiOptions.json)
1641
- console.log(JSON.stringify({ operation: 'ui-add', paths: [], components: [] }, null, 2));
1740
+ console.log(JSON.stringify({
1741
+ operation: 'ui-add',
1742
+ status: 'unchanged',
1743
+ generatorVersion: GENERATOR_VERSION,
1744
+ rootDirectory: root,
1745
+ paths: [],
1746
+ components: [],
1747
+ providerFiles: 'unchanged',
1748
+ installCommands: [],
1749
+ nextSteps: [],
1750
+ }, null, 2));
1642
1751
  else
1643
1752
  anhedralPrint.info('All requested UI components are already installed in the selected clients.');
1644
1753
  return;
@@ -1649,8 +1758,28 @@ export async function scaffoldUiComponents(uiOptions) {
1649
1758
  provider: entry.provider,
1650
1759
  source: entry.source,
1651
1760
  }));
1761
+ const installCommands = buildUiInstallCommands(root, additions).map((command) => ({
1762
+ target: command.target,
1763
+ cwd: path.relative(root, command.cwd),
1764
+ command: [command.executable, ...command.args].join(' '),
1765
+ }));
1652
1766
  if (uiOptions.json) {
1653
- console.log(JSON.stringify({ operation: 'ui-add', paths: planned, components: componentPlan }, null, 2));
1767
+ console.log(JSON.stringify({
1768
+ operation: 'ui-add',
1769
+ status: uiOptions.dryRun ? 'planned' : 'applied',
1770
+ generatorVersion: GENERATOR_VERSION,
1771
+ rootDirectory: root,
1772
+ paths: planned,
1773
+ components: componentPlan,
1774
+ providerFiles: uiOptions.dryRun ? 'resolved-on-apply' : 'applied',
1775
+ installCommands,
1776
+ nextSteps: uiOptions.dryRun
1777
+ ? ['Review the plan, then re-run the same command without --dry-run.']
1778
+ : [
1779
+ ...(uiOptions.skipInstall ? ['pnpm install'] : []),
1780
+ 'pnpm verify',
1781
+ ],
1782
+ }, null, 2));
1654
1783
  }
1655
1784
  else {
1656
1785
  anhedralPrint.info(`ui-add plan: ${componentPlan.length} installation${componentPlan.length === 1 ? '' : 's'}`);
@@ -1658,6 +1787,11 @@ export async function scaffoldUiComponents(uiOptions) {
1658
1787
  console.log(` ${entry.target}: ${entry.component} (${entry.provider})`);
1659
1788
  for (const relativePath of planned)
1660
1789
  console.log(` ${relativePath}`);
1790
+ if (uiOptions.dryRun) {
1791
+ anhedralPrint.info('Registry file paths are resolved by the UI provider only when you apply this plan.');
1792
+ for (const command of installCommands)
1793
+ console.log(` ${command.cwd}: ${command.command}`);
1794
+ }
1661
1795
  }
1662
1796
  }
1663
1797
  finally {
@@ -1724,12 +1858,23 @@ export async function scaffoldUpgradeProject(upgradeOptions) {
1724
1858
  });
1725
1859
  if (noOp) {
1726
1860
  if (upgradeOptions.json)
1727
- console.log(JSON.stringify({ operation: 'upgrade', paths: [] }, null, 2));
1861
+ console.log(JSON.stringify({
1862
+ operation: 'upgrade',
1863
+ status: 'unchanged',
1864
+ generatorVersion: GENERATOR_VERSION,
1865
+ rootDirectory: root,
1866
+ paths: [],
1867
+ nextSteps: [],
1868
+ }, null, 2));
1728
1869
  else
1729
1870
  anhedralPrint.info(`Project is already on Anhedral ${GENERATOR_VERSION}.`);
1730
1871
  return;
1731
1872
  }
1732
- printPlan('upgrade', planned, upgradeOptions.json);
1873
+ printPlan('upgrade', planned, upgradeOptions.json, resolveModules(options?.modules ?? manifest?.modules ?? []), {
1874
+ dryRun: upgradeOptions.dryRun,
1875
+ skipInstall: upgradeOptions.skipInstall,
1876
+ rootDirectory: root,
1877
+ });
1733
1878
  }
1734
1879
  finally {
1735
1880
  if (previousChannel == null)
@@ -1794,16 +1939,38 @@ export async function scaffoldAddModules(addOptions) {
1794
1939
  },
1795
1940
  });
1796
1941
  if (noOp) {
1797
- if (addOptions.json)
1798
- console.log(JSON.stringify({ operation: 'add', paths: [] }, null, 2));
1942
+ if (addOptions.json) {
1943
+ const resolution = resolveModules([...(manifest?.modules ?? []), ...addOptions.modules]);
1944
+ console.log(JSON.stringify({
1945
+ operation: 'add',
1946
+ status: 'unchanged',
1947
+ generatorVersion: GENERATOR_VERSION,
1948
+ rootDirectory: root,
1949
+ requestedModules: resolution.requestedModules,
1950
+ resolvedModules: resolution.resolvedModules,
1951
+ dependencyAddedModules: resolution.dependencyAddedModules,
1952
+ dependencyEdges: resolution.dependencyEdges,
1953
+ paths: [],
1954
+ nextSteps: [],
1955
+ }, null, 2));
1956
+ }
1799
1957
  else
1800
1958
  anhedralPrint.info('All requested modules are already installed.');
1801
1959
  return;
1802
1960
  }
1961
+ const resolution = resolveModules(options?.modules ?? manifest?.modules ?? addOptions.modules);
1803
1962
  if (addOptions.dryRun || !addOptions.json)
1804
- printPlan('add', planned, addOptions.json);
1963
+ printPlan('add', planned, addOptions.json, resolution, {
1964
+ dryRun: addOptions.dryRun,
1965
+ skipInstall: addOptions.skipInstall,
1966
+ rootDirectory: root,
1967
+ });
1805
1968
  if (!addOptions.dryRun && addOptions.json)
1806
- printPlan('add', planned, true);
1969
+ printPlan('add', planned, true, resolution, {
1970
+ dryRun: false,
1971
+ skipInstall: addOptions.skipInstall,
1972
+ rootDirectory: root,
1973
+ });
1807
1974
  }
1808
1975
  finally {
1809
1976
  if (previousChannel == null)
@@ -1837,7 +2004,7 @@ export function doctorProject() {
1837
2004
  path: 'anhedral.json',
1838
2005
  severity: 'error',
1839
2006
  message: isSupportedProjectUpgrade(manifest.generatorVersion, GENERATOR_VERSION)
1840
- ? `Project generator ${manifest.generatorVersion} differs from CLI ${GENERATOR_VERSION}; run anhedral upgrade.`
2007
+ ? `Project generator ${manifest.generatorVersion} differs from CLI ${GENERATOR_VERSION}; run pnpm anhedral:upgrade.`
1841
2008
  : `Project generator ${manifest.generatorVersion} differs from CLI ${GENERATOR_VERSION}; regenerate with the current CLI.`,
1842
2009
  });
1843
2010
  }
@@ -1886,10 +2053,50 @@ export function doctorProject() {
1886
2053
  }
1887
2054
  }
1888
2055
  }
2056
+ const recommendedActions = [];
2057
+ if (manifest.generatorVersion !== GENERATOR_VERSION) {
2058
+ recommendedActions.push(isSupportedProjectUpgrade(manifest.generatorVersion, GENERATOR_VERSION)
2059
+ ? 'Run `pnpm anhedral:upgrade --dry-run`, inspect the plan, then run `pnpm anhedral:upgrade`.'
2060
+ : 'Generate a fresh project with the current CLI and move product-owned code into its extension seams.');
2061
+ }
2062
+ if (issues.some((issue) => isTransactionMetadata(issue.path))) {
2063
+ recommendedActions.push('Re-run the interrupted Anhedral command without `--dry-run` so its transaction can recover.');
2064
+ }
2065
+ if (issues.some((issue) => issue.severity === 'error' && manifest.files[issue.path]?.ownership === 'managed')) {
2066
+ recommendedActions.push('Restore managed files from source control, then run `pnpm anhedral:doctor` again.');
2067
+ }
2068
+ if (issues.some((issue) => issue.severity === 'error' && manifest.files[issue.path]?.ownership === 'user')) {
2069
+ recommendedActions.push('Restore recorded user-owned paths as regular files, then run `pnpm anhedral:doctor` again.');
2070
+ }
2071
+ if (issues.some((issue) => issue.severity === 'error' && manifest.files[issue.path]?.ownership === 'mergeable')) {
2072
+ recommendedActions.push('Restore mergeable workspace configuration from source control, then run `pnpm anhedral:doctor` again.');
2073
+ }
2074
+ if (issues.some((issue) => issue.severity === 'warning')) {
2075
+ recommendedActions.push('Review user-owned file warnings; intentional product edits are safe and do not block Anhedral operations.');
2076
+ }
2077
+ issues.sort((left, right) => {
2078
+ if (left.path !== right.path)
2079
+ return left.path < right.path ? -1 : 1;
2080
+ if (left.severity !== right.severity)
2081
+ return left.severity === 'error' ? -1 : 1;
2082
+ if (left.message === right.message)
2083
+ return 0;
2084
+ return left.message < right.message ? -1 : 1;
2085
+ });
2086
+ const ownership = Object.values(manifest.files).reduce((summary, record) => {
2087
+ summary[record.ownership] += 1;
2088
+ summary.total += 1;
2089
+ return summary;
2090
+ }, { managed: 0, mergeable: 0, user: 0, total: 0 });
1889
2091
  return {
1890
2092
  ok: !issues.some((issue) => issue.severity === 'error'),
1891
2093
  schemaVersion: manifest.schemaVersion,
1892
2094
  generatorVersion: manifest.generatorVersion,
2095
+ project: manifest.project,
2096
+ modules: manifest.modules,
2097
+ toolchain: manifest.toolchain,
2098
+ ownership,
1893
2099
  issues,
2100
+ recommendedActions,
1894
2101
  };
1895
2102
  }