anhedral 0.4.0 → 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 (52) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +76 -57
  4. package/SKILL.md +107 -12
  5. package/assets/anhedral-cli-init.svg +238 -0
  6. package/assets/images/svg/ably-symbol-color.svg +8 -0
  7. package/assets/images/svg/logo-white-subtract.svg +4 -0
  8. package/dist/architecture/index.d.ts +2 -2
  9. package/dist/architecture/index.js +1 -1
  10. package/dist/architecture/modules.d.ts +5 -1
  11. package/dist/architecture/modules.js +4 -1
  12. package/dist/bin.js +113 -84
  13. package/dist/cli.d.ts +7 -7
  14. package/dist/cli.js +62 -16
  15. package/dist/dependencies.d.ts +1 -0
  16. package/dist/dependencies.js +11 -8
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +1 -1
  19. package/dist/project.d.ts +30 -0
  20. package/dist/project.js +1 -0
  21. package/dist/prompts.d.ts +4 -3
  22. package/dist/prompts.js +29 -7
  23. package/dist/scaffold.d.ts +12 -24
  24. package/dist/scaffold.js +345 -124
  25. package/dist/templates/api.d.ts +1 -1
  26. package/dist/templates/api.js +67 -2
  27. package/dist/templates/assets-private-proxy.d.ts +1 -1
  28. package/dist/templates/desktop.d.ts +1 -1
  29. package/dist/templates/desktop.js +189 -36
  30. package/dist/templates/electron-updater.d.ts +1 -1
  31. package/dist/templates/electron-updater.js +1 -1
  32. package/dist/templates/extension.d.ts +1 -1
  33. package/dist/templates/extension.js +138 -14
  34. package/dist/templates/first-run.d.ts +2 -0
  35. package/dist/templates/first-run.js +167 -0
  36. package/dist/templates/mobile.d.ts +1 -1
  37. package/dist/templates/mobile.js +158 -17
  38. package/dist/templates/shared.d.ts +1 -1
  39. package/dist/templates/shared.js +64 -7
  40. package/dist/templates/web.d.ts +1 -1
  41. package/dist/templates/web.js +185 -28
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/docs/NORTHSTAR.md +114 -0
  45. package/docs/conventions.md +6 -4
  46. package/docs/engineering-principles.md +88 -0
  47. package/docs/master-stack-map.md +7 -0
  48. package/docs/output-tree-contract.md +16 -5
  49. package/docs/references/manual-scaffolding.md +22 -4
  50. package/docs/references/provisioning.md +434 -77
  51. package/package.json +10 -2
  52. package/scripts/render-master-stack-map.mjs +510 -0
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)
@@ -171,7 +173,21 @@ function toolchainChannelFromManifest(manifest) {
171
173
  return resolveToolchainChannel(manifest.toolchain);
172
174
  }
173
175
  export function isSupportedProjectUpgrade(fromVersion, toVersion) {
174
- return fromVersion === '0.3.0' && toVersion === '0.4.0';
176
+ const parseStableVersion = (version) => {
177
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
178
+ return match
179
+ ? [Number(match[1]), Number(match[2]), Number(match[3])]
180
+ : undefined;
181
+ };
182
+ const from = parseStableVersion(fromVersion);
183
+ const to = parseStableVersion(toVersion);
184
+ if (!from || !to)
185
+ return false;
186
+ const supportedOwnershipMigration = fromVersion === '0.3.0' && to[0] === 0 && to[1] === 4;
187
+ const compatiblePatchUpgrade = from[0] === to[0]
188
+ && from[1] === to[1]
189
+ && from[2] < to[2];
190
+ return supportedOwnershipMigration || compatiblePatchUpgrade;
175
191
  }
176
192
  function ensureScaffoldRoot(root) {
177
193
  const rootStat = lstatIfPresent(root);
@@ -199,12 +215,18 @@ function rootScripts(options) {
199
215
  ? [primaryClient, ...(options.apps.api ? ['./apps/api'] : [])]
200
216
  : filters.slice(0, 1);
201
217
  const scripts = {
202
- 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."',
203
221
  ...(filters.length > primaryFilters.length
204
- ? { 'dev:all': `turbo dev --parallel ${filters.map((entry) => `--filter=${entry}`).join(' ')}` }
222
+ ? { 'dev:all': `turbo run dev ${filters.map((entry) => `--filter=${entry}`).join(' ')}` }
205
223
  : {}),
206
224
  build: 'turbo build',
207
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',
208
230
  };
209
231
  const verify = [];
210
232
  if (options.apps.web) {
@@ -509,15 +531,22 @@ function writeRootEnv(root, options) {
509
531
  const separator = current && additions.length ? (current.endsWith('\n') ? '\n' : '\n\n') : '';
510
532
  writeFile(filePath, `${current}${separator}${additions.join('\n')}${additions.length ? '\n' : ''}`);
511
533
  }
512
- 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));
513
541
  return {
514
542
  $schema: 'https://turborepo.dev/schema.json',
515
543
  tasks: {
516
544
  build: {
517
545
  dependsOn: ['^build'],
518
- outputs: ['.next/**', '!.next/cache/**', '.output/**', 'dist/**'],
546
+ outputs: [],
519
547
  },
520
- typecheck: { dependsOn: ['^build'] },
548
+ ...appBuildTasks,
549
+ typecheck: { dependsOn: ['^typecheck'] },
521
550
  dev: { cache: false, persistent: true },
522
551
  },
523
552
  };
@@ -676,7 +705,7 @@ function writeRootFiles(root, options, mode, currentRoot, manifest) {
676
705
  options.apps.api || options.features.database ? 'packages/*' : null,
677
706
  ].filter((value) => value !== null);
678
707
  mergeWorkspaceFile(root, workspacePackages, desiredWorkspacePolicy(), workspaceUnmodified);
679
- mergeJsonConfig(root, 'turbo.json', desiredTurboConfig(), desiredTurboConfig(), true);
708
+ mergeJsonConfig(root, 'turbo.json', desiredTurboConfig(options), desiredTurboConfig(options), true);
680
709
  mergeJsonConfig(root, 'vercel.json', desiredVercelConfig(options), previousOptions ? desiredVercelConfig(previousOptions) : {}, true);
681
710
  appendGitignore(root, [
682
711
  'node_modules',
@@ -707,6 +736,7 @@ function writeRootFiles(root, options, mode, currentRoot, manifest) {
707
736
  'apps/desktop-updater-worker',
708
737
  ])];
709
738
  writeFile(vercelIgnore, ignoreLines.join('\n') + '\n');
739
+ writeFile(path.join(root, 'scripts/first-run.mjs'), generatedFirstRunScript(options));
710
740
  writeFile(path.join(root, '.github/workflows/anhedral-ci.yml'), generatedCi(options));
711
741
  }
712
742
  function enabledModuleNames(options) {
@@ -715,17 +745,11 @@ function enabledModuleNames(options) {
715
745
  function writeProjectDocs(root, options, includeUserDocs) {
716
746
  const modules = enabledModuleNames(options);
717
747
  const storageBucketName = r2BucketName(options.projectName);
718
- const environmentSetupCommands = [
719
- 'pnpm install',
720
- options.apps.api ? 'cp apps/api/.env.example apps/api/.env' : null,
721
- options.features.database ? 'cp packages/db/.env.example packages/db/.env' : null,
722
- options.apps.web ? 'cp apps/web/.env.example apps/web/.env.local' : null,
723
- options.apps.mobile ? 'cp apps/mobile/.env.example apps/mobile/.env' : null,
724
- options.apps.desktop ? 'cp apps/desktop/.env.example apps/desktop/.env' : null,
725
- options.features.electronUpdater ? 'cp apps/desktop/electron-builder.env.example apps/desktop/electron-builder.env' : null,
726
- options.apps.extension ? 'cp apps/extension/.env.example apps/extension/.env' : null,
727
- ].filter((value) => value !== null).join('\n');
748
+ const environmentSetupCommands = `pnpm install # only if generation used --skip-install
749
+ pnpm first-run
750
+ pnpm ready`;
728
751
  const firstVerificationCommands = [
752
+ options.features.database ? 'git rev-parse --is-inside-work-tree >/dev/null 2>&1 || git init' : null,
729
753
  options.features.database ? 'pnpm db:generate' : null,
730
754
  options.features.database ? 'git add packages/db/migrations' : null,
731
755
  'pnpm verify',
@@ -747,14 +771,14 @@ function writeProjectDocs(root, options, includeUserDocs) {
747
771
  options.apps.mobile ? 'pnpm mobile:eas:login' : null,
748
772
  options.apps.mobile ? 'pnpm mobile:build:internal:ios' : null,
749
773
  options.apps.mobile ? 'pnpm mobile:build:internal:android' : null,
750
- options.features.database ? `pnpm neon:project:create -- --name ${options.projectName}` : null,
774
+ options.features.database ? `pnpm neon:project:create --name ${options.projectName}` : null,
751
775
  options.features.storage ? 'pnpm r2:bucket:create' : null,
752
776
  options.features.storage ? 'pnpm assets:proxy:check' : null,
753
777
  options.features.storage ? 'pnpm assets:proxy:deploy' : null,
754
778
  options.features.electronUpdater ? 'pnpm desktop:updates:cloudflare:login' : null,
755
779
  options.features.electronUpdater ? 'pnpm desktop:updates:first-provision' : null,
756
780
  options.features.electronUpdater ? 'pnpm desktop:updates:build:mac' : null,
757
- 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,
758
782
  options.apps.extension ? 'pnpm extension:zip' : null,
759
783
  ].filter((value) => value !== null).join('\n');
760
784
  const sourceRows = [
@@ -814,11 +838,12 @@ Edit \`packages/db/src/app-schema.ts\`, then run:
814
838
 
815
839
  \`\`\`sh
816
840
  pnpm db:generate
841
+ git add packages/db/migrations
817
842
  pnpm verify:db
818
843
  pnpm db:migrate
819
844
  \`\`\`
820
845
 
821
- 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,
822
847
  options.features.auth ? `### Use authentication
823
848
 
824
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,
@@ -826,57 +851,22 @@ Frontend applications use their generated Clerk provider and hooks. The API veri
826
851
 
827
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,
828
853
  ].filter((value) => value !== null).join('\n\n');
829
- const verticalSliceExample = options.apps.api && options.features.database ? `## Copy-paste example: a projects feature
830
-
831
- These files are user-owned extension seams. Anhedral preserves them during \`add\`, \`ui add\`, and \`upgrade\`.
832
-
833
- \`packages/contracts/src/app.ts\`:
834
-
835
- \`\`\`ts
836
- import { z } from 'zod';
837
-
838
- export const ProjectSchema = z.object({ id: z.string(), name: z.string() });
839
- export const ProjectListSchema = z.array(ProjectSchema);
840
- \`\`\`
841
-
842
- \`packages/db/src/app-schema.ts\`:
843
-
844
- \`\`\`ts
845
- import { pgTable, text } from 'drizzle-orm/pg-core';
846
-
847
- export const projects = pgTable('projects', {
848
- id: text('id').primaryKey(),
849
- name: text('name').notNull(),
850
- });
851
- \`\`\`
852
-
853
- \`apps/api/src/routes/app.ts\`:
854
-
855
- \`\`\`ts
856
- import type { FastifyPluginAsync } from 'fastify';
857
- import { ProjectListSchema } from '@shared/contracts';
858
- import { db } from '@shared/db';
859
- import { projects } from '@shared/db/schema';
854
+ const verticalSliceExample = options.apps.api && options.features.database ? `## Your working starter feature
860
855
 
861
- export const appRoutes: FastifyPluginAsync = async (app) => {
862
- app.get('/projects', async () => ProjectListSchema.parse(await db.select().from(projects)));
863
- };
864
- \`\`\`
865
-
866
- ${options.apps.web || options.apps.mobile || options.apps.desktop || options.apps.extension ? `\`packages/api-client/src/app.ts\`:
867
-
868
- \`\`\`ts
869
- import { ProjectListSchema } from '@shared/contracts';
870
- import { ApiClient } from './generated';
856
+ Anhedral generated an editable **items** feature all the way through the stack:
871
857
 
872
- export function listProjects(client: ApiClient) {
873
- return client.request('/projects', { method: 'GET' }, ProjectListSchema);
874
- }
875
- \`\`\`
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.' : ''}
876
866
 
877
- Import \`listProjects\` and the generated client factory in the frontend surface you are building.` : ''}
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\`.
878
868
 
879
- 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\`.` : '';
880
870
  const dependencyLines = [
881
871
  options.apps.api ? 'frontend apps -> @shared/api-client -> HTTP -> apps/api' : null,
882
872
  options.apps.api ? 'frontend apps -> @shared/contracts <- apps/api' : null,
@@ -887,8 +877,8 @@ Then run \`pnpm db:generate\`, review and commit the migration, run \`pnpm verif
887
877
  ? `### Add UI primitives
888
878
 
889
879
  \`\`\`sh
890
- anhedral ui add dialog
891
- ${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' : ''}\`\`\`
892
882
 
893
883
  DOM apps receive source-owned shadcn/ui files. Mobile receives React Native Reusables files. Customize those files normally.`
894
884
  : '';
@@ -905,13 +895,24 @@ Anhedral installs workspace dependencies during generation unless you used \`--s
905
895
  ${environmentSetupCommands}
906
896
  \`\`\`
907
897
 
908
- 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:
909
899
 
910
900
  \`\`\`sh
911
901
  ${firstVerificationCommands}
912
902
  \`\`\`
913
903
 
914
- 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:
915
916
 
916
917
  ${moduleCommands}
917
918
 
@@ -931,11 +932,11 @@ ${modules.map((moduleName) => `- \`${moduleName}\``).join('\n')}
931
932
 
932
933
  ## UI and customization
933
934
 
934
- 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.
935
936
 
936
937
  ## Environment and first verification
937
938
 
938
- 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.' : ''}
939
940
 
940
941
  ## Deployment
941
942
 
@@ -949,7 +950,7 @@ ${deploymentRows}
949
950
  ${deploymentCommands}
950
951
  \`\`\`
951
952
 
952
- 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\`.
953
954
  `);
954
955
  }
955
956
  writeFile(path.join(root, 'docs/DEVELOPMENT.md'), `# Developing ${markdownHeading(options.displayName)}
@@ -971,17 +972,17 @@ ${uiTaskSection}
971
972
  ### Add another app surface
972
973
 
973
974
  \`\`\`sh
974
- anhedral add mobile --dry-run
975
- anhedral add mobile
975
+ pnpm anhedral:add mobile --dry-run
976
+ pnpm anhedral:add mobile
976
977
  \`\`\`
977
978
 
978
- 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.
979
980
 
980
981
  ## Verification
981
982
 
982
983
  - While iterating, run the affected package's typecheck or tests.
983
984
  - Before handoff, run \`pnpm verify\`.
984
- - Before generation, run \`anhedral doctor\` and use \`--dry-run\`.
985
+ - Before generation, run \`pnpm anhedral:doctor\` and use \`--dry-run\`.
985
986
  - For schema changes, commit reviewed migration SQL and run \`pnpm verify:db\`.
986
987
  `);
987
988
  writeFile(path.join(root, 'docs/STACK.md'), `# Stack guide
@@ -1000,7 +1001,7 @@ ${dependencyLines || 'selected applications are independent framework projects'}
1000
1001
 
1001
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.
1002
1003
 
1003
- \`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.
1004
1005
  `);
1005
1006
  if (includeUserDocs) {
1006
1007
  const productionItems = [
@@ -1018,27 +1019,28 @@ Clients may import contracts and the API client. They must not import API servic
1018
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,
1019
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,
1020
1021
  '- Run `pnpm verify` before deployment.',
1021
- '- Run `anhedral doctor` before incremental generation.',
1022
+ '- Run `pnpm anhedral:doctor` before incremental generation.',
1022
1023
  ].filter((item) => item !== null);
1023
1024
  const productionSections = [
1024
- 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
1025
1026
 
1026
- 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.
1027
1028
 
1028
- 1. Buy the domain at GoDaddy under an organization-controlled account. Enable auto-renewal, MFA, accurate registrant contact details, and record renewal ownership.
1029
- 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.
1030
- 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.
1031
- 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.
1032
- 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.
1033
1033
 
1034
- 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,
1035
1037
  options.apps.web || options.apps.api ? `## Vercel: web and API
1036
1038
 
1037
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.
1038
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.
1039
- 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.
1040
- 4. Run \`pnpm deploy:vercel:inspect -- <deployment-url>\` when diagnosing a deployment. Verify health, authentication, scheduled jobs, and provider callbacks in Preview before merging.
1041
- 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.
1042
1044
 
1043
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,
1044
1046
  options.features.auth ? `## Clerk: production identity
@@ -1049,7 +1051,7 @@ When web and API are both selected, keep the generated \`/api/(.*)\` route befor
1049
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,
1050
1052
  options.features.database ? `## Neon and Drizzle: database
1051
1053
 
1052
- 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.
1053
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.
1054
1056
  3. Change the Drizzle schema, run \`pnpm db:generate\`, review and commit the SQL and metadata, then run \`pnpm verify:db\`.
1055
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.
@@ -1089,7 +1091,7 @@ The generated update path is \`electron-updater -> updates.<domain> Worker -> pr
1089
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.
1090
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.
1091
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.
1092
- 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.
1093
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.
1094
1096
 
1095
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
@@ -1108,7 +1110,7 @@ ${productionSections.join('\n\n')}
1108
1110
 
1109
1111
  ## Final release order
1110
1112
 
1111
- - Run \`pnpm verify\` and \`anhedral doctor\`; review the exact production diff.
1113
+ - Run \`pnpm verify\` and \`pnpm anhedral:doctor\`; review the exact production diff.
1112
1114
  ${options.features.database ? '- Apply reviewed, backward-compatible database migrations.' : ''}
1113
1115
  ${options.apps.web || options.apps.api ? '- Deploy provider infrastructure and secrets, then deploy API/web.' : ''}
1114
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.' : ''}
@@ -1120,11 +1122,11 @@ Generator: ${GENERATOR_VERSION}
1120
1122
 
1121
1123
  Resolved modules: ${modules.join(', ')}
1122
1124
 
1123
- - \`anhedral add <module> --dry-run\` previews incremental changes.
1124
- - \`anhedral upgrade --dry-run\` previews a supported generator migration.
1125
- - \`anhedral ui add <component> --dry-run\` previews platform-routed component additions.
1126
- - \`anhedral doctor\` reports manifest and filesystem drift before incremental changes.
1127
- - 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.
1128
1130
  `);
1129
1131
  const surfaceGuidance = [
1130
1132
  options.apps.web ? '- Web lives in `apps/web`: use Next.js App Router, server-first rendering, and shadcn/ui source components.' : null,
@@ -1170,9 +1172,10 @@ Before implementing a feature, read \`README.md\` for the source map, \`docs/DEV
1170
1172
  ## Start safely
1171
1173
 
1172
1174
  1. Read \`anhedral.json\` for the selected modules, native styling provider, installed UI components, and file ownership.
1173
- 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.
1174
- 3. Preview structural changes with \`anhedral upgrade --dry-run\`, \`anhedral add <module> --dry-run\`, or \`anhedral ui add <component> --dry-run\` as applicable.
1175
- 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.
1176
1179
 
1177
1180
  Resolved modules: ${modules.map((moduleName) => `\`${moduleName}\``).join(', ')}.
1178
1181
 
@@ -1189,7 +1192,7 @@ ${skillFeatureSteps}
1189
1192
 
1190
1193
  ## UI conventions
1191
1194
 
1192
- - 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.
1193
1196
  - Web, desktop, and extension components come from shadcn/ui. Mobile components come from React Native Reusables using ${options.nativeStyling}.
1194
1197
  - Compose application-specific UI around installed primitives. Preserve accessibility, keyboard behavior, focus handling, and platform conventions.
1195
1198
  - Use lowercase kebab-case filenames, PascalCase component exports, and \`use-\` prefixes for hooks.
@@ -1197,7 +1200,7 @@ ${skillFeatureSteps}
1197
1200
  ## Ownership rules
1198
1201
 
1199
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.
1200
- - 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.
1201
1204
  - Never overwrite user changes, bypass a generator conflict, or replace a failed ownership check with fabricated hashes.
1202
1205
 
1203
1206
  ## Environment and security
@@ -1218,7 +1221,11 @@ When the user asks you to provision this project:
1218
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.
1219
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.
1220
1223
 
1221
- 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.'}
1222
1229
 
1223
1230
  - Read \`PRODUCTION.md\` before provisioning or changing production resources. It is tailored to this project's selected surfaces and providers.
1224
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.' : ''}
@@ -1234,9 +1241,10 @@ Run the strongest applicable checks before handing work back:
1234
1241
 
1235
1242
  \`\`\`sh
1236
1243
  pnpm typecheck
1244
+ pnpm ready
1237
1245
  pnpm verify
1238
1246
  pnpm build
1239
- pnpm dlx anhedral@${GENERATOR_VERSION} doctor
1247
+ pnpm anhedral:doctor
1240
1248
  \`\`\`
1241
1249
 
1242
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.
@@ -1386,7 +1394,7 @@ function readProjectManifest(root, options = {}) {
1386
1394
  return manifest;
1387
1395
  }
1388
1396
  const nextStep = isSupportedProjectUpgrade(manifest.generatorVersion, GENERATOR_VERSION)
1389
- ? 'Run anhedral upgrade before adding modules.'
1397
+ ? 'Run `pnpm anhedral:upgrade` before adding modules.'
1390
1398
  : 'Regenerate the project with the current CLI before adding modules.';
1391
1399
  throw new Error(`This project was generated by Anhedral ${manifest.generatorVersion}; current CLI ${GENERATOR_VERSION} only supports exact-current projects. `
1392
1400
  + nextStep);
@@ -1410,15 +1418,66 @@ function cleanNestedArtifacts(root) {
1410
1418
  function selectedUiTargets(options) {
1411
1419
  return UI_TARGETS.filter((target) => options.apps[target]);
1412
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
+ }
1413
1442
  function installSelectedUiComponents(root, options) {
1414
1443
  const installs = resolveUiInstalls(options.uiComponents, selectedUiTargets(options), options.nativeStyling);
1415
1444
  if (installs.length > 0 && !options.dryRun)
1416
1445
  installUiComponents(root, installs);
1417
- return installs;
1446
+ return mergeUiInstalls(bundledUiComponents(options), installs);
1418
1447
  }
1419
1448
  function installWorkspaceDependencies(root) {
1420
1449
  const executable = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
1450
+ anhedralPrint.step('Installing workspace dependencies');
1421
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');
1422
1481
  }
1423
1482
  async function writeSelectedModules(root, options) {
1424
1483
  const templates = materializeTemplates(root, resolveModules(options.modules).resolvedModules);
@@ -1440,14 +1499,56 @@ async function writeSelectedModules(root, options) {
1440
1499
  scaffoldElectronUpdater(root, shared);
1441
1500
  return templates;
1442
1501
  }
1443
- 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);
1444
1515
  if (json) {
1445
- 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));
1446
1530
  return;
1447
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
+ }
1448
1539
  anhedralPrint.info(`${operation} plan: ${paths.length} path${paths.length === 1 ? '' : 's'}`);
1449
- for (const relativePath of paths)
1450
- 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
+ }
1451
1552
  }
1452
1553
  function assertSafeChangedPaths(root, stageRoot, manifest, paths, deleting = new Set()) {
1453
1554
  for (const relativePath of paths) {
@@ -1529,6 +1630,10 @@ export async function scaffoldProject(inputOptions) {
1529
1630
  commitPaths.push(...readdirSync(stageRoot));
1530
1631
  },
1531
1632
  afterCommit: () => {
1633
+ if (options.initializeGit) {
1634
+ anhedralPrint.section('Version control');
1635
+ initializeGitRepository(root);
1636
+ }
1532
1637
  if (!options.skipInstall) {
1533
1638
  anhedralPrint.section('Workspace install');
1534
1639
  installWorkspaceDependencies(root);
@@ -1536,11 +1641,19 @@ export async function scaffoldProject(inputOptions) {
1536
1641
  },
1537
1642
  });
1538
1643
  if (options.dryRun)
1539
- 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
+ });
1540
1649
  if (!options.dryRun)
1541
1650
  anhedralPrint.done(`Committed ${planned.length} paths`);
1542
1651
  if (!options.dryRun && options.json)
1543
- printPlan('init', planned, true);
1652
+ printPlan('init', planned, true, resolveModules(options.modules), {
1653
+ dryRun: false,
1654
+ skipInstall: options.skipInstall,
1655
+ rootDirectory: root,
1656
+ });
1544
1657
  }
1545
1658
  finally {
1546
1659
  if (previousChannel == null)
@@ -1624,7 +1737,17 @@ export async function scaffoldUiComponents(uiOptions) {
1624
1737
  });
1625
1738
  if (noOp) {
1626
1739
  if (uiOptions.json)
1627
- 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));
1628
1751
  else
1629
1752
  anhedralPrint.info('All requested UI components are already installed in the selected clients.');
1630
1753
  return;
@@ -1635,8 +1758,28 @@ export async function scaffoldUiComponents(uiOptions) {
1635
1758
  provider: entry.provider,
1636
1759
  source: entry.source,
1637
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
+ }));
1638
1766
  if (uiOptions.json) {
1639
- 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));
1640
1783
  }
1641
1784
  else {
1642
1785
  anhedralPrint.info(`ui-add plan: ${componentPlan.length} installation${componentPlan.length === 1 ? '' : 's'}`);
@@ -1644,6 +1787,11 @@ export async function scaffoldUiComponents(uiOptions) {
1644
1787
  console.log(` ${entry.target}: ${entry.component} (${entry.provider})`);
1645
1788
  for (const relativePath of planned)
1646
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
+ }
1647
1795
  }
1648
1796
  }
1649
1797
  finally {
@@ -1710,12 +1858,23 @@ export async function scaffoldUpgradeProject(upgradeOptions) {
1710
1858
  });
1711
1859
  if (noOp) {
1712
1860
  if (upgradeOptions.json)
1713
- 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));
1714
1869
  else
1715
1870
  anhedralPrint.info(`Project is already on Anhedral ${GENERATOR_VERSION}.`);
1716
1871
  return;
1717
1872
  }
1718
- 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
+ });
1719
1878
  }
1720
1879
  finally {
1721
1880
  if (previousChannel == null)
@@ -1780,16 +1939,38 @@ export async function scaffoldAddModules(addOptions) {
1780
1939
  },
1781
1940
  });
1782
1941
  if (noOp) {
1783
- if (addOptions.json)
1784
- 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
+ }
1785
1957
  else
1786
1958
  anhedralPrint.info('All requested modules are already installed.');
1787
1959
  return;
1788
1960
  }
1961
+ const resolution = resolveModules(options?.modules ?? manifest?.modules ?? addOptions.modules);
1789
1962
  if (addOptions.dryRun || !addOptions.json)
1790
- printPlan('add', planned, addOptions.json);
1963
+ printPlan('add', planned, addOptions.json, resolution, {
1964
+ dryRun: addOptions.dryRun,
1965
+ skipInstall: addOptions.skipInstall,
1966
+ rootDirectory: root,
1967
+ });
1791
1968
  if (!addOptions.dryRun && addOptions.json)
1792
- printPlan('add', planned, true);
1969
+ printPlan('add', planned, true, resolution, {
1970
+ dryRun: false,
1971
+ skipInstall: addOptions.skipInstall,
1972
+ rootDirectory: root,
1973
+ });
1793
1974
  }
1794
1975
  finally {
1795
1976
  if (previousChannel == null)
@@ -1823,7 +2004,7 @@ export function doctorProject() {
1823
2004
  path: 'anhedral.json',
1824
2005
  severity: 'error',
1825
2006
  message: isSupportedProjectUpgrade(manifest.generatorVersion, GENERATOR_VERSION)
1826
- ? `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.`
1827
2008
  : `Project generator ${manifest.generatorVersion} differs from CLI ${GENERATOR_VERSION}; regenerate with the current CLI.`,
1828
2009
  });
1829
2010
  }
@@ -1872,10 +2053,50 @@ export function doctorProject() {
1872
2053
  }
1873
2054
  }
1874
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 });
1875
2091
  return {
1876
2092
  ok: !issues.some((issue) => issue.severity === 'error'),
1877
2093
  schemaVersion: manifest.schemaVersion,
1878
2094
  generatorVersion: manifest.generatorVersion,
2095
+ project: manifest.project,
2096
+ modules: manifest.modules,
2097
+ toolchain: manifest.toolchain,
2098
+ ownership,
1879
2099
  issues,
2100
+ recommendedActions,
1880
2101
  };
1881
2102
  }