anhedral 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +68 -55
- package/SKILL.md +107 -12
- package/dist/architecture/index.d.ts +2 -2
- package/dist/architecture/index.js +1 -1
- package/dist/architecture/modules.d.ts +5 -1
- package/dist/architecture/modules.js +4 -1
- package/dist/bin.js +113 -84
- package/dist/cli.d.ts +7 -7
- package/dist/cli.js +62 -16
- package/dist/dependencies.d.ts +1 -0
- package/dist/dependencies.js +11 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/project.d.ts +30 -0
- package/dist/project.js +1 -0
- package/dist/prompts.d.ts +4 -3
- package/dist/prompts.js +29 -7
- package/dist/scaffold.d.ts +12 -24
- package/dist/scaffold.js +330 -123
- package/dist/templates/api.d.ts +1 -1
- package/dist/templates/api.js +67 -2
- package/dist/templates/assets-private-proxy.d.ts +1 -1
- package/dist/templates/desktop.d.ts +1 -1
- package/dist/templates/desktop.js +189 -36
- package/dist/templates/electron-updater.d.ts +1 -1
- package/dist/templates/electron-updater.js +1 -1
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +138 -14
- package/dist/templates/first-run.d.ts +2 -0
- package/dist/templates/first-run.js +167 -0
- package/dist/templates/mobile.d.ts +1 -1
- package/dist/templates/mobile.js +158 -17
- package/dist/templates/shared.d.ts +1 -1
- package/dist/templates/shared.js +64 -7
- package/dist/templates/web.d.ts +1 -1
- package/dist/templates/web.js +185 -28
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/NORTHSTAR.md +114 -0
- package/docs/conventions.md +6 -4
- package/docs/engineering-principles.md +88 -0
- package/docs/output-tree-contract.md +16 -5
- package/docs/references/manual-scaffolding.md +22 -4
- package/docs/references/provisioning.md +434 -77
- package/package.json +3 -1
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
|
-
|
|
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
|
|
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: [
|
|
546
|
+
outputs: [],
|
|
533
547
|
},
|
|
534
|
-
|
|
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
|
-
|
|
734
|
-
|
|
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 --
|
|
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 --
|
|
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 ? `##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
905
|
-
${options.apps.web ? 'anhedral
|
|
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.
|
|
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\`.
|
|
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}
|
|
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
|
|
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
|
|
989
|
-
anhedral
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1022
|
+
'- Run `pnpm anhedral:doctor` before incremental generation.',
|
|
1036
1023
|
].filter((item) => item !== null);
|
|
1037
1024
|
const productionSections = [
|
|
1038
|
-
options.
|
|
1025
|
+
options.features.storage || options.features.electronUpdater ? `## Domain and Cloudflare control plane
|
|
1039
1026
|
|
|
1040
|
-
|
|
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.
|
|
1043
|
-
2.
|
|
1044
|
-
3.
|
|
1045
|
-
4.
|
|
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
|
|
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.
|
|
1054
|
-
4. Run \`pnpm deploy:vercel:inspect
|
|
1055
|
-
5.
|
|
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 --
|
|
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 --
|
|
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
|
|
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
|
|
1138
|
-
- \`anhedral
|
|
1139
|
-
- \`anhedral
|
|
1140
|
-
- \`anhedral
|
|
1141
|
-
- README and existing user workflows are never rewritten by
|
|
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
|
|
1188
|
-
3.
|
|
1189
|
-
4.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
|
|
1464
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
|
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
|
}
|