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.
- package/CHANGELOG.md +59 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +76 -57
- package/SKILL.md +107 -12
- package/assets/anhedral-cli-init.svg +238 -0
- package/assets/images/svg/ably-symbol-color.svg +8 -0
- package/assets/images/svg/logo-white-subtract.svg +4 -0
- 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 +345 -124
- 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/master-stack-map.md +7 -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 +10 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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: [
|
|
546
|
+
outputs: [],
|
|
519
547
|
},
|
|
520
|
-
|
|
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
|
-
|
|
720
|
-
|
|
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 --
|
|
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 --
|
|
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 ? `##
|
|
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
|
-
|
|
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
|
-
|
|
873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
891
|
-
${options.apps.web ? 'anhedral
|
|
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.
|
|
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\`.
|
|
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}
|
|
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
|
|
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
|
|
975
|
-
anhedral
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1022
|
+
'- Run `pnpm anhedral:doctor` before incremental generation.',
|
|
1022
1023
|
].filter((item) => item !== null);
|
|
1023
1024
|
const productionSections = [
|
|
1024
|
-
options.
|
|
1025
|
+
options.features.storage || options.features.electronUpdater ? `## Domain and Cloudflare control plane
|
|
1025
1026
|
|
|
1026
|
-
|
|
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.
|
|
1029
|
-
2.
|
|
1030
|
-
3.
|
|
1031
|
-
4.
|
|
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
|
|
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.
|
|
1040
|
-
4. Run \`pnpm deploy:vercel:inspect
|
|
1041
|
-
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.
|
|
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 --
|
|
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 --
|
|
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
|
|
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
|
|
1124
|
-
- \`anhedral
|
|
1125
|
-
- \`anhedral
|
|
1126
|
-
- \`anhedral
|
|
1127
|
-
- 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.
|
|
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
|
|
1174
|
-
3.
|
|
1175
|
-
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
|
|
1450
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
|
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
|
}
|