anhedral 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +68 -55
  4. package/SKILL.md +107 -12
  5. package/dist/architecture/index.d.ts +2 -2
  6. package/dist/architecture/index.js +1 -1
  7. package/dist/architecture/modules.d.ts +5 -1
  8. package/dist/architecture/modules.js +4 -1
  9. package/dist/bin.js +113 -84
  10. package/dist/cli.d.ts +7 -7
  11. package/dist/cli.js +62 -16
  12. package/dist/dependencies.d.ts +1 -0
  13. package/dist/dependencies.js +11 -8
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +1 -1
  16. package/dist/project.d.ts +30 -0
  17. package/dist/project.js +1 -0
  18. package/dist/prompts.d.ts +4 -3
  19. package/dist/prompts.js +29 -7
  20. package/dist/scaffold.d.ts +12 -24
  21. package/dist/scaffold.js +330 -123
  22. package/dist/templates/api.d.ts +1 -1
  23. package/dist/templates/api.js +67 -2
  24. package/dist/templates/assets-private-proxy.d.ts +1 -1
  25. package/dist/templates/desktop.d.ts +1 -1
  26. package/dist/templates/desktop.js +189 -36
  27. package/dist/templates/electron-updater.d.ts +1 -1
  28. package/dist/templates/electron-updater.js +1 -1
  29. package/dist/templates/extension.d.ts +1 -1
  30. package/dist/templates/extension.js +138 -14
  31. package/dist/templates/first-run.d.ts +2 -0
  32. package/dist/templates/first-run.js +167 -0
  33. package/dist/templates/mobile.d.ts +1 -1
  34. package/dist/templates/mobile.js +158 -17
  35. package/dist/templates/shared.d.ts +1 -1
  36. package/dist/templates/shared.js +64 -7
  37. package/dist/templates/web.d.ts +1 -1
  38. package/dist/templates/web.js +185 -28
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/docs/NORTHSTAR.md +114 -0
  42. package/docs/conventions.md +6 -4
  43. package/docs/engineering-principles.md +88 -0
  44. package/docs/output-tree-contract.md +16 -5
  45. package/docs/references/manual-scaffolding.md +22 -4
  46. package/docs/references/provisioning.md +434 -77
  47. package/package.json +3 -1
package/dist/bin.js CHANGED
@@ -1,11 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from 'node:readline/promises';
3
3
  import { argv, stdin, stdout } from 'node:process';
4
- import { buildAddOptions, buildOptions, buildOptionsForRoot, parseCli, parseNewProjectRequest, parseUiAddOptions, USAGE } from './cli.js';
4
+ import { APP_MODULES, FEATURE_MODULES, buildAddOptions, buildOptions, buildOptionsForRoot, parseCli, parseNewProjectRequest, parseUiAddOptions, USAGE, } from './cli.js';
5
5
  import { doctorProject, scaffoldAddModules, scaffoldProject, scaffoldUiComponents, scaffoldUpgradeProject } from './scaffold.js';
6
- import { DEFAULT_PROMPT_APP_MODULES, DEFAULT_PROMPT_FEATURE_MODULES, hasUiSelection, parsePromptModules, shouldPromptForInitModules, } from './prompts.js';
6
+ import { DEFAULT_PROMPT_APP_MODULES, DEFAULT_PROMPT_FEATURE_MODULES, hasUiSelection, parsePromptConfirmation, parsePromptModuleSelection, shouldPromptForInitModules, } from './prompts.js';
7
7
  import { GENERATOR_VERSION } from './version.js';
8
8
  import { PostCommitError } from './transaction.js';
9
+ import { resolveModules } from './architecture/modules.js';
10
+ import { UI_TARGETS } from './ui.js';
11
+ const COMMANDS = ['new', 'init', 'add', 'ui', 'upgrade', 'doctor'];
12
+ const COMMAND_SET = new Set(COMMANDS);
13
+ const UI_TARGET_SET = new Set(UI_TARGETS);
14
+ function isCommand(value) {
15
+ return COMMAND_SET.has(value);
16
+ }
9
17
  function writeCliError(error, code, json) {
10
18
  const message = error instanceof Error ? error.message : String(error);
11
19
  if (json)
@@ -13,52 +21,42 @@ function writeCliError(error, code, json) {
13
21
  else
14
22
  console.error('Error:', message);
15
23
  }
16
- function splitAddArgs(args) {
17
- const moduleNames = [];
18
- const flagArgs = [];
19
- for (let index = 0; index < args.length; index += 1) {
20
- const arg = args[index];
21
- if (arg === '--toolchain') {
22
- flagArgs.push(arg);
23
- if (args[index + 1] != null) {
24
- flagArgs.push(args[index + 1]);
25
- index += 1;
26
- }
27
- continue;
28
- }
29
- if (arg.startsWith('--')) {
30
- flagArgs.push(arg);
31
- continue;
32
- }
33
- moduleNames.push(arg);
34
- }
35
- return { moduleNames, flagArgs };
36
- }
37
24
  async function promptForInitModules(args) {
38
25
  if (!shouldPromptForInitModules(args, stdin.isTTY === true))
39
26
  return args;
40
27
  const rl = createInterface({ input: stdin, output: stdout });
41
28
  try {
42
- console.log('Select app surfaces: web, mobile, api, desktop, extension');
43
- const appAnswer = await rl.question('App surfaces [all]: ');
44
- console.log('Select backend features: db, auth, billing, storage, native-subscriptions, electron-updater');
45
- const featureAnswer = await rl.question('Backend features [all]: ');
29
+ console.log('Select app surfaces: web, mobile, api, desktop, extension (or "all"/"none")');
30
+ const appAnswer = await rl.question(`App surfaces [${DEFAULT_PROMPT_APP_MODULES.join(', ')}]: `);
31
+ console.log('Select capabilities: db, auth, billing, storage, native-subscriptions, electron-updater (or "all"/"none")');
32
+ const featureAnswer = await rl.question('Capabilities [none]: ');
46
33
  const selected = [
47
- ...parsePromptModules(appAnswer, DEFAULT_PROMPT_APP_MODULES),
48
- ...parsePromptModules(featureAnswer, DEFAULT_PROMPT_FEATURE_MODULES),
34
+ ...parsePromptModuleSelection(appAnswer, DEFAULT_PROMPT_APP_MODULES, APP_MODULES),
35
+ ...parsePromptModuleSelection(featureAnswer, DEFAULT_PROMPT_FEATURE_MODULES, FEATURE_MODULES),
49
36
  ];
37
+ if (selected.length === 0)
38
+ throw new Error('Select at least one app surface or capability.');
39
+ const resolution = resolveModules(selected);
50
40
  const result = [...args, ...selected.map((moduleName) => `--${moduleName}`)];
51
- const hasUiClient = selected.some((moduleName) => ['web', 'mobile', 'desktop', 'extension'].includes(moduleName));
41
+ const hasUiClient = resolution.resolvedModules.some((moduleName) => UI_TARGET_SET.has(moduleName));
52
42
  if (hasUiClient && !hasUiSelection(args)) {
53
43
  console.log('Optionally add starter UI components to each selected client.');
54
44
  const componentAnswer = await rl.question('Starter components [none]: ');
55
45
  if (componentAnswer.trim())
56
46
  result.push(`--ui=${componentAnswer}`);
57
47
  }
58
- if (selected.includes('mobile') && !args.some((arg) => arg.startsWith('--native-styling'))) {
48
+ if (resolution.resolvedModules.includes('mobile') && !args.some((arg) => arg.startsWith('--native-styling'))) {
59
49
  const stylingAnswer = (await rl.question('Expo styling [nativewind]: ')).trim();
60
50
  result.push(`--native-styling=${stylingAnswer || 'nativewind'}`);
61
51
  }
52
+ console.log(`Requested: ${resolution.requestedModules.join(', ')}`);
53
+ if (resolution.dependencyAddedModules.length > 0) {
54
+ console.log(`Added by dependencies: ${resolution.dependencyAddedModules.join(', ')}`);
55
+ }
56
+ console.log(`Resolved stack: ${resolution.resolvedModules.join(', ')}`);
57
+ const confirmation = await rl.question('Generate this stack? [Y/n]: ');
58
+ if (!parsePromptConfirmation(confirmation))
59
+ throw new Error('Generation cancelled.');
62
60
  return result;
63
61
  }
64
62
  finally {
@@ -89,7 +87,7 @@ async function main() {
89
87
  console.log(USAGE);
90
88
  return;
91
89
  }
92
- if (!['new', 'init', 'add', 'ui', 'upgrade', 'doctor'].includes(command)) {
90
+ if (!isCommand(command)) {
93
91
  if (json)
94
92
  writeCliError(new Error(`Unknown command: ${command}`), 'UNKNOWN_COMMAND', true);
95
93
  else {
@@ -108,60 +106,91 @@ async function main() {
108
106
  }
109
107
  let phase = 'arguments';
110
108
  try {
111
- if (command === 'new') {
112
- const request = parseNewProjectRequest(rawArgs);
113
- const promptedArgs = await promptForInitModules([...request.moduleArgs]);
114
- const options = buildOptionsForRoot(parseCli(promptedArgs), request.directory);
115
- phase = 'execute';
116
- await scaffoldProject(options);
117
- if (!options.json && !options.dryRun) {
118
- console.log(`\nCreated ${options.rootDirectory}\nNext: cd ${options.rootDirectory}\nThen follow README.md to configure the selected providers and start the app.`);
109
+ switch (command) {
110
+ case 'new': {
111
+ const request = parseNewProjectRequest(rawArgs);
112
+ const promptedArgs = await promptForInitModules([...request.moduleArgs]);
113
+ const parsed = parseCli(promptedArgs);
114
+ const options = {
115
+ ...buildOptionsForRoot(parsed, request.directory),
116
+ initializeGit: parsed.initializeGit ?? true,
117
+ };
118
+ phase = 'execute';
119
+ await scaffoldProject(options);
120
+ if (!options.json && !options.dryRun) {
121
+ console.log(`\nCreated ${options.rootDirectory}\n\nNext:`);
122
+ console.log(` cd ${options.rootDirectory}`);
123
+ if (options.skipInstall)
124
+ console.log(' pnpm install');
125
+ console.log(' pnpm first-run');
126
+ console.log(' pnpm ready');
127
+ console.log('\nThen follow README.md for verification, development, and deployment.');
128
+ }
129
+ break;
119
130
  }
120
- }
121
- else if (command === 'doctor') {
122
- const unknown = rawArgs.filter((arg) => !['--json', '--verbose'].includes(arg));
123
- if (unknown.length)
124
- throw new Error(`Unknown doctor option: ${unknown[0]}`);
125
- phase = 'execute';
126
- const report = doctorProject();
127
- if (json)
128
- console.log(JSON.stringify(report, null, 2));
129
- else {
130
- console.log(report.ok ? 'Anhedral project is healthy.' : 'Anhedral project has drift.');
131
- for (const issue of report.issues)
132
- console.log(` ${issue.severity}: ${issue.path}: ${issue.message}`);
131
+ case 'doctor': {
132
+ const unknown = rawArgs.filter((arg) => !['--json', '--verbose'].includes(arg));
133
+ if (unknown.length)
134
+ throw new Error(`Unknown doctor option: ${unknown[0]}`);
135
+ phase = 'execute';
136
+ const report = doctorProject();
137
+ if (json)
138
+ console.log(JSON.stringify(report, null, 2));
139
+ else {
140
+ console.log(report.ok ? 'Anhedral project is healthy.' : 'Anhedral project has drift.');
141
+ console.log(` project: ${report.project.displayName} (${report.project.name})`);
142
+ console.log(` modules: ${report.modules.join(', ')}`);
143
+ console.log(` toolchain: ${report.toolchain}`);
144
+ console.log(` ownership: ${report.ownership.user} user, ${report.ownership.mergeable} mergeable, `
145
+ + `${report.ownership.managed} managed`);
146
+ for (const issue of report.issues)
147
+ console.log(` ${issue.severity}: ${issue.path}: ${issue.message}`);
148
+ if (report.recommendedActions.length > 0) {
149
+ console.log('\nNext actions:');
150
+ for (const action of report.recommendedActions)
151
+ console.log(` - ${action}`);
152
+ }
153
+ }
154
+ if (!report.ok)
155
+ process.exitCode = 1;
156
+ break;
157
+ }
158
+ case 'add': {
159
+ const options = buildAddOptions([], parseCli(rawArgs));
160
+ phase = 'execute';
161
+ await scaffoldAddModules(options);
162
+ break;
163
+ }
164
+ case 'ui': {
165
+ if (rawArgs[0] !== 'add')
166
+ throw new Error('Unknown UI command. Use: anhedral ui add <component...>');
167
+ const options = parseUiAddOptions(rawArgs.slice(1));
168
+ phase = 'execute';
169
+ await scaffoldUiComponents(options);
170
+ break;
171
+ }
172
+ case 'upgrade': {
173
+ const unknown = rawArgs.filter((arg) => !['--skip-install', '--dry-run', '--json', '--verbose'].includes(arg));
174
+ if (unknown.length)
175
+ throw new Error(`Unknown upgrade option: ${unknown[0]}`);
176
+ phase = 'execute';
177
+ await scaffoldUpgradeProject({
178
+ skipInstall: rawArgs.includes('--skip-install'),
179
+ dryRun: rawArgs.includes('--dry-run'),
180
+ json,
181
+ });
182
+ break;
183
+ }
184
+ case 'init': {
185
+ const parsed = parseCli(await promptForInitModules(rawArgs));
186
+ const options = {
187
+ ...buildOptions(parsed),
188
+ initializeGit: parsed.initializeGit ?? false,
189
+ };
190
+ phase = 'execute';
191
+ await scaffoldProject(options);
192
+ break;
133
193
  }
134
- if (!report.ok)
135
- process.exitCode = 1;
136
- }
137
- else if (command === 'add') {
138
- const { moduleNames, flagArgs } = splitAddArgs(rawArgs);
139
- const options = buildAddOptions(moduleNames, parseCli(flagArgs));
140
- phase = 'execute';
141
- await scaffoldAddModules(options);
142
- }
143
- else if (command === 'ui') {
144
- if (rawArgs[0] !== 'add')
145
- throw new Error('Unknown UI command. Use: anhedral ui add <component...>');
146
- const options = parseUiAddOptions(rawArgs.slice(1));
147
- phase = 'execute';
148
- await scaffoldUiComponents(options);
149
- }
150
- else if (command === 'upgrade') {
151
- const unknown = rawArgs.filter((arg) => !['--skip-install', '--dry-run', '--json', '--verbose'].includes(arg));
152
- if (unknown.length)
153
- throw new Error(`Unknown upgrade option: ${unknown[0]}`);
154
- phase = 'execute';
155
- await scaffoldUpgradeProject({
156
- skipInstall: rawArgs.includes('--skip-install'),
157
- dryRun: rawArgs.includes('--dry-run'),
158
- json,
159
- });
160
- }
161
- else {
162
- const options = buildOptions(parseCli(await promptForInitModules(rawArgs)));
163
- phase = 'execute';
164
- await scaffoldProject(options);
165
194
  }
166
195
  }
167
196
  catch (error) {
package/dist/cli.d.ts CHANGED
@@ -1,28 +1,28 @@
1
1
  import type { AddOptions, InitOptions } from './scaffold.js';
2
+ import { APP_MODULES, FEATURE_MODULES, type AppModule, type FeatureModule, type ModuleId } from './architecture/modules.js';
2
3
  import { type NativeStylingLibrary } from './ui.js';
3
4
  import type { UiAddOptions } from './scaffold.js';
4
- export declare const USAGE = "\nanhedral new <directory> [modules...] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral init [modules...] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral add <module...> [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral ui add <component...> [--target <client>] [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral upgrade [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral doctor [--json] [--verbose]\nanhedral --version\n\nCommands:\n anhedral new my-app\n Generate the complete TypeScript stack in a new directory. With no module flags, every module is included.\n anhedral new my-app --web --api --db --auth\n Generate a web app, Fastify API, shared database package, and auth wiring.\n anhedral init --web --api --db --auth\n Generate the same readable workspace in the current empty directory.\n anhedral add mobile extension\n Add missing modules to an existing Anhedral project.\n anhedral ui add button dialog --target mobile\n Add React Native Reusables components to Expo. DOM clients use shadcn/ui.\n anhedral upgrade\n Transactionally upgrade a supported older Anhedral project before adding modules.\n";
5
+ export declare const USAGE = "\nanhedral new <directory> [modules...|--all] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--no-git] [--dry-run] [--json] [--verbose]\nanhedral init [modules...|--all] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--git] [--dry-run] [--json] [--verbose]\nanhedral add <module...|--all> [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral ui add <component...> [--target <client>] [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral upgrade [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral doctor [--json] [--verbose]\nanhedral --version\n\nCommands:\n anhedral new my-app\n Interactively choose a focused stack in a terminal. In noninteractive use, pass module flags; no flags explicitly means the complete stack.\n anhedral new my-app --web --api --db --auth\n Generate a web app, Fastify API, shared database package, and auth wiring.\n anhedral init --web --api --db --auth\n Generate the same readable workspace in the current empty directory.\n anhedral add mobile extension\n Add missing modules to an existing Anhedral project.\n anhedral ui add button dialog --target mobile\n Add React Native Reusables components to Expo. DOM clients use shadcn/ui.\n anhedral upgrade\n Transactionally upgrade a supported older Anhedral project before adding modules.\n\nApp surfaces:\n web Next.js App Router application\n mobile Expo Router application for iOS, Android, and web\n api Fastify HTTP API\n desktop Electron desktop application\n extension WXT browser extension\n\nCapabilities:\n db Neon Postgres + Drizzle\n auth Clerk; adds api + db\n billing RevenueCat + Stripe + Ably; adds auth\n storage Private Cloudflare R2; adds auth\n native-subscriptions Native RevenueCat client; adds mobile + billing\n electron-updater Private Electron update channel; adds desktop\n\nBehavior:\n --all explicitly selects every app surface and capability.\n new initializes Git when Git is available; use --no-git to opt out.\n init preserves the current directory's repository state; use --git to initialize Git.\n --dry-run never writes the destination. --json emits stable machine-readable plans.\n Expo selections require Node ^22.13.0 or ^24.3.0. Other stacks support Node ^20.19.0 or newer supported releases.\n";
5
6
  export type NewProjectRequest = {
6
7
  readonly directory: string;
7
8
  readonly moduleArgs: readonly string[];
8
9
  };
9
10
  export declare function parseNewProjectRequest(args: readonly string[]): NewProjectRequest;
10
- export declare const APP_MODULES: readonly ["web", "mobile", "api", "desktop", "extension"];
11
- export declare const FEATURE_MODULES: readonly ["db", "auth", "billing", "storage", "native-subscriptions", "electron-updater"];
12
- export type AppModule = (typeof APP_MODULES)[number];
13
- export type FeatureModule = (typeof FEATURE_MODULES)[number];
14
- export type SupportedModule = AppModule | FeatureModule;
11
+ export { APP_MODULES, FEATURE_MODULES };
12
+ export type { AppModule, FeatureModule };
13
+ export type SupportedModule = ModuleId;
15
14
  export type ParsedFlags = {
16
15
  toolchain?: string;
17
16
  skipInstall?: boolean;
18
17
  dryRun?: boolean;
19
18
  json?: boolean;
20
19
  verbose?: boolean;
20
+ initializeGit?: boolean;
21
21
  uiComponents: string[];
22
22
  nativeStyling?: NativeStylingLibrary;
23
23
  modules: Set<SupportedModule>;
24
24
  };
25
- export declare function parseCli(args: string[]): ParsedFlags;
25
+ export declare function parseCli(args: readonly string[]): ParsedFlags;
26
26
  export declare function normalizeModuleName(raw: string): SupportedModule | null;
27
27
  export declare function deriveProjectName(cwd: string): string;
28
28
  export declare function deriveDisplayName(cwd: string): string;
package/dist/cli.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { env } from 'node:process';
3
3
  import { TOOLCHAIN_CHANNELS, resolveToolchainChannel } from './toolchain.js';
4
- import { resolveModules } from './architecture/modules.js';
4
+ import { APP_MODULES, FEATURE_MODULES, isModuleId, resolveModules, } from './architecture/modules.js';
5
5
  import { packageNameFromText } from './render.js';
6
6
  import { isNativeStylingLibrary, isUiTarget, parseUiComponentList, } from './ui.js';
7
7
  export const USAGE = `
8
- anhedral new <directory> [modules...] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--dry-run] [--json] [--verbose]
9
- anhedral init [modules...] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--dry-run] [--json] [--verbose]
10
- anhedral add <module...> [--skip-install] [--dry-run] [--json] [--verbose]
8
+ anhedral new <directory> [modules...|--all] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--no-git] [--dry-run] [--json] [--verbose]
9
+ anhedral init [modules...|--all] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--git] [--dry-run] [--json] [--verbose]
10
+ anhedral add <module...|--all> [--skip-install] [--dry-run] [--json] [--verbose]
11
11
  anhedral ui add <component...> [--target <client>] [--skip-install] [--dry-run] [--json] [--verbose]
12
12
  anhedral upgrade [--skip-install] [--dry-run] [--json] [--verbose]
13
13
  anhedral doctor [--json] [--verbose]
@@ -15,7 +15,7 @@ anhedral --version
15
15
 
16
16
  Commands:
17
17
  anhedral new my-app
18
- Generate the complete TypeScript stack in a new directory. With no module flags, every module is included.
18
+ Interactively choose a focused stack in a terminal. In noninteractive use, pass module flags; no flags explicitly means the complete stack.
19
19
  anhedral new my-app --web --api --db --auth
20
20
  Generate a web app, Fastify API, shared database package, and auth wiring.
21
21
  anhedral init --web --api --db --auth
@@ -26,6 +26,28 @@ Commands:
26
26
  Add React Native Reusables components to Expo. DOM clients use shadcn/ui.
27
27
  anhedral upgrade
28
28
  Transactionally upgrade a supported older Anhedral project before adding modules.
29
+
30
+ App surfaces:
31
+ web Next.js App Router application
32
+ mobile Expo Router application for iOS, Android, and web
33
+ api Fastify HTTP API
34
+ desktop Electron desktop application
35
+ extension WXT browser extension
36
+
37
+ Capabilities:
38
+ db Neon Postgres + Drizzle
39
+ auth Clerk; adds api + db
40
+ billing RevenueCat + Stripe + Ably; adds auth
41
+ storage Private Cloudflare R2; adds auth
42
+ native-subscriptions Native RevenueCat client; adds mobile + billing
43
+ electron-updater Private Electron update channel; adds desktop
44
+
45
+ Behavior:
46
+ --all explicitly selects every app surface and capability.
47
+ new initializes Git when Git is available; use --no-git to opt out.
48
+ init preserves the current directory's repository state; use --git to initialize Git.
49
+ --dry-run never writes the destination. --json emits stable machine-readable plans.
50
+ Expo selections require Node ^22.13.0 or ^24.3.0. Other stacks support Node ^20.19.0 or newer supported releases.
29
51
  `;
30
52
  export function parseNewProjectRequest(args) {
31
53
  const [directory, ...moduleArgs] = args;
@@ -33,8 +55,13 @@ export function parseNewProjectRequest(args) {
33
55
  throw new Error('anhedral new requires a destination directory before module flags');
34
56
  return Object.freeze({ directory, moduleArgs: Object.freeze(moduleArgs) });
35
57
  }
36
- export const APP_MODULES = ['web', 'mobile', 'api', 'desktop', 'extension'];
37
- export const FEATURE_MODULES = ['db', 'auth', 'billing', 'storage', 'native-subscriptions', 'electron-updater'];
58
+ export { APP_MODULES, FEATURE_MODULES };
59
+ function assignOption(option, current, next) {
60
+ if (current !== undefined && current !== next) {
61
+ throw new Error(`Conflicting values for ${option}: ${String(current)} and ${String(next)}`);
62
+ }
63
+ return next;
64
+ }
38
65
  export function parseCli(args) {
39
66
  const flags = { modules: new Set(), uiComponents: [] };
40
67
  for (let index = 0; index < args.length; index += 1) {
@@ -63,6 +90,19 @@ export function parseCli(args) {
63
90
  flags.verbose = true;
64
91
  continue;
65
92
  }
93
+ if (token === '--git') {
94
+ flags.initializeGit = assignOption('--git/--no-git', flags.initializeGit, true);
95
+ continue;
96
+ }
97
+ if (token === '--no-git') {
98
+ flags.initializeGit = assignOption('--git/--no-git', flags.initializeGit, false);
99
+ continue;
100
+ }
101
+ if (token === '--all') {
102
+ for (const moduleName of [...APP_MODULES, ...FEATURE_MODULES])
103
+ flags.modules.add(moduleName);
104
+ continue;
105
+ }
66
106
  if (token === '--ui') {
67
107
  const value = args[index + 1];
68
108
  if (!value || value.startsWith('--'))
@@ -79,7 +119,7 @@ export function parseCli(args) {
79
119
  const value = args[index + 1];
80
120
  if (!isNativeStylingLibrary(value))
81
121
  throw new Error('--native-styling must be nativewind or uniwind');
82
- flags.nativeStyling = value;
122
+ flags.nativeStyling = assignOption('--native-styling', flags.nativeStyling, value);
83
123
  index += 1;
84
124
  continue;
85
125
  }
@@ -87,7 +127,7 @@ export function parseCli(args) {
87
127
  const value = token.slice('--native-styling='.length);
88
128
  if (!isNativeStylingLibrary(value))
89
129
  throw new Error('--native-styling must be nativewind or uniwind');
90
- flags.nativeStyling = value;
130
+ flags.nativeStyling = assignOption('--native-styling', flags.nativeStyling, value);
91
131
  continue;
92
132
  }
93
133
  if (token === '--toolchain') {
@@ -95,7 +135,7 @@ export function parseCli(args) {
95
135
  if (!value || value.startsWith('--')) {
96
136
  throw new Error('Missing value for --toolchain');
97
137
  }
98
- flags.toolchain = value;
138
+ flags.toolchain = assignOption('--toolchain', flags.toolchain, value);
99
139
  index += 1;
100
140
  continue;
101
141
  }
@@ -104,7 +144,7 @@ export function parseCli(args) {
104
144
  if (!value) {
105
145
  throw new Error('Missing value for --toolchain');
106
146
  }
107
- flags.toolchain = value;
147
+ flags.toolchain = assignOption('--toolchain', flags.toolchain, value);
108
148
  continue;
109
149
  }
110
150
  const moduleName = normalizeModuleName(token.slice(2));
@@ -117,11 +157,7 @@ export function parseCli(args) {
117
157
  return flags;
118
158
  }
119
159
  export function normalizeModuleName(raw) {
120
- if (APP_MODULES.includes(raw))
121
- return raw;
122
- if (FEATURE_MODULES.includes(raw))
123
- return raw;
124
- return null;
160
+ return isModuleId(raw) ? raw : null;
125
161
  }
126
162
  export function deriveProjectName(cwd) {
127
163
  return packageNameFromText(path.basename(cwd));
@@ -155,6 +191,7 @@ export function buildOptionsForRoot(flags, root) {
155
191
  json: flags.json === true,
156
192
  toolchainChannel: resolveToolchainChannel(flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN),
157
193
  rootDirectory: resolvedRoot,
194
+ initializeGit: flags.initializeGit,
158
195
  };
159
196
  }
160
197
  export function parseUiAddOptions(args) {
@@ -212,6 +249,15 @@ export function parseUiAddOptions(args) {
212
249
  };
213
250
  }
214
251
  export function buildAddOptions(modules, flags) {
252
+ if (flags.initializeGit !== undefined) {
253
+ throw new Error('Git initialization options are only supported by anhedral new and anhedral init');
254
+ }
255
+ if (flags.uiComponents.length > 0) {
256
+ throw new Error('Use anhedral ui add <component...> to add UI components');
257
+ }
258
+ if (flags.nativeStyling !== undefined) {
259
+ throw new Error('--native-styling is only supported while creating a project');
260
+ }
215
261
  const requestedModules = [...modules, ...flags.modules];
216
262
  if (requestedModules.length === 0) {
217
263
  throw new Error('anhedral add requires at least one module');
@@ -24,6 +24,7 @@ export declare const REALTIME_DEPENDENCIES: DependencyGroup;
24
24
  export declare const BACKEND_DEPENDENCIES: DependencyGroup;
25
25
  export declare const EXTENSION_DEPENDENCIES: DependencyGroup;
26
26
  export declare const FRONTEND_ADDON_DEPENDENCIES: DependencyMap;
27
+ export declare const CLERK_SOLANA_PEER_DEPENDENCIES: DependencyMap;
27
28
  export declare const MOBILE_APP_DEPENDENCIES: DependencyGroup;
28
29
  export declare const MOBILE_NATIVEWIND_DEPENDENCIES: DependencyGroup;
29
30
  export declare const MOBILE_UNIWIND_DEPENDENCIES: DependencyGroup;
@@ -2,7 +2,7 @@ export const PACKAGE_MANAGER = 'pnpm@10.34.5';
2
2
  export const NODE_ENGINE = '^20.19.0 || >=22.12.0';
3
3
  // Expo SDK 56 targets React Native 0.85, whose supported Node release lines start here.
4
4
  export const MOBILE_NODE_ENGINE = '^22.13.0 || ^24.3.0 || >=25';
5
- const VERIFIED_AT = '2026-07-15';
5
+ const VERIFIED_AT = '2026-07-23';
6
6
  export const TOOLCHAIN_DEPENDENCIES = {
7
7
  // renovate: datasource=npm depName=shadcn
8
8
  shadcn: '4.13.0',
@@ -175,6 +175,14 @@ export const FRONTEND_ADDON_DEPENDENCIES = {
175
175
  // renovate: datasource=npm depName=react-native-purchases-ui
176
176
  'react-native-purchases-ui': '10.4.2',
177
177
  };
178
+ export const CLERK_SOLANA_PEER_DEPENDENCIES = {
179
+ // Clerk's wallet adapters expect these peers to be explicit when pnpm's
180
+ // autoInstallPeers setting is disabled.
181
+ // renovate: datasource=npm depName=@solana/web3.js
182
+ '@solana/web3.js': '1.98.4',
183
+ // renovate: datasource=npm depName=bs58
184
+ bs58: '6.0.0',
185
+ };
178
186
  export const MOBILE_APP_DEPENDENCIES = {
179
187
  dependencies: {
180
188
  '@shared/api-client': 'workspace:*',
@@ -187,7 +195,7 @@ export const MOBILE_APP_DEPENDENCIES = {
187
195
  // renovate: datasource=npm depName=expo
188
196
  expo: '56.0.16',
189
197
  // renovate: datasource=npm depName=expo-constants
190
- 'expo-constants': '56.0.21',
198
+ 'expo-constants': '56.0.22',
191
199
  // renovate: datasource=npm depName=expo-linking
192
200
  'expo-linking': '56.0.15',
193
201
  // renovate: datasource=npm depName=expo-router
@@ -290,12 +298,7 @@ export const DESKTOP_DEPENDENCIES = {
290
298
  // renovate: datasource=npm depName=@clerk/clerk-js
291
299
  '@clerk/clerk-js': '6.25.3',
292
300
  '@clerk/ui': WEB_APP_DEPENDENCIES.dependencies['@clerk/ui'],
293
- // Clerk's browser bundle imports @solana/wallet-adapter-react, whose web3 peer must be explicit when autoInstallPeers is disabled.
294
- // renovate: datasource=npm depName=@solana/web3.js
295
- '@solana/web3.js': '1.98.4',
296
- // Clerk's Solana wallet adapter expects the current bs58 peer alongside web3.js.
297
- // renovate: datasource=npm depName=bs58
298
- bs58: '6.0.0',
301
+ ...CLERK_SOLANA_PEER_DEPENDENCIES,
299
302
  react: EXTENSION_DEPENDENCIES.dependencies.react,
300
303
  'react-dom': EXTENSION_DEPENDENCIES.dependencies['react-dom'],
301
304
  clsx: EXTENSION_DEPENDENCIES.dependencies.clsx,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { APP_MODULES, FEATURE_MODULES, USAGE, buildAddOptions, buildOptions, deriveDisplayName, deriveProjectName, normalizeModuleName, parseCli, parseNewProjectRequest, buildOptionsForRoot, parseUiAddOptions, } from './cli.js';
2
- export type { AppModule, FeatureModule, ParsedFlags, SupportedModule } from './cli.js';
1
+ export { USAGE, buildAddOptions, buildOptions, deriveDisplayName, deriveProjectName, normalizeModuleName, parseCli, parseNewProjectRequest, buildOptionsForRoot, parseUiAddOptions, } from './cli.js';
2
+ export type { ParsedFlags, SupportedModule } from './cli.js';
3
3
  export { doctorProject, isSupportedProjectUpgrade, scaffoldAddModules, scaffoldProject, scaffoldUiComponents, scaffoldUpgradeProject, } from './scaffold.js';
4
4
  export type { AddOptions, DoctorIssue, DoctorReport, InitOptions, ProjectOptions, UiAddOptions, UpgradeOptions, } from './scaffold.js';
5
5
  export * from './architecture/index.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { APP_MODULES, FEATURE_MODULES, USAGE, buildAddOptions, buildOptions, deriveDisplayName, deriveProjectName, normalizeModuleName, parseCli, parseNewProjectRequest, buildOptionsForRoot, parseUiAddOptions, } from './cli.js';
1
+ export { USAGE, buildAddOptions, buildOptions, deriveDisplayName, deriveProjectName, normalizeModuleName, parseCli, parseNewProjectRequest, buildOptionsForRoot, parseUiAddOptions, } from './cli.js';
2
2
  export { doctorProject, isSupportedProjectUpgrade, scaffoldAddModules, scaffoldProject, scaffoldUiComponents, scaffoldUpgradeProject, } from './scaffold.js';
3
3
  export * from './architecture/index.js';
4
4
  export { BundledTemplateSource, assertTemplateProvenance, hashTemplateDirectory, materializeTemplates, templateIdsForModules, } from './template-source.js';
@@ -0,0 +1,30 @@
1
+ import type { NativeStylingLibrary } from './ui.js';
2
+ export type AppSelections = {
3
+ web: boolean;
4
+ mobile: boolean;
5
+ api: boolean;
6
+ desktop: boolean;
7
+ extension: boolean;
8
+ };
9
+ export type FeatureSelections = {
10
+ database: boolean;
11
+ auth: boolean;
12
+ billing: boolean;
13
+ storage: boolean;
14
+ nativeSubscriptions: boolean;
15
+ electronUpdater: boolean;
16
+ };
17
+ /**
18
+ * The project shape consumed by every template.
19
+ *
20
+ * Keep this independent from the scaffold orchestrator so templates depend on
21
+ * the project model, not on the command that happens to create it.
22
+ */
23
+ export interface ProjectOptions {
24
+ projectName: string;
25
+ displayName: string;
26
+ apps: AppSelections;
27
+ features: FeatureSelections;
28
+ skipInstall?: boolean;
29
+ nativeStyling?: NativeStylingLibrary;
30
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/prompts.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export declare function shouldPromptForInitModules(args: readonly string[], isTTY: boolean): boolean;
2
2
  export declare function hasUiSelection(args: readonly string[]): boolean;
3
- export declare function parsePromptModules(input: string, fallback: readonly string[]): string[];
4
- export declare const DEFAULT_PROMPT_APP_MODULES: readonly ["web", "mobile", "api", "desktop", "extension"];
5
- export declare const DEFAULT_PROMPT_FEATURE_MODULES: readonly ["db", "auth", "billing", "storage", "native-subscriptions", "electron-updater"];
3
+ export declare function parsePromptModuleSelection(input: string, fallback: readonly string[], all: readonly string[]): string[];
4
+ export declare function parsePromptConfirmation(input: string): boolean;
5
+ export declare const DEFAULT_PROMPT_APP_MODULES: readonly ["web"];
6
+ export declare const DEFAULT_PROMPT_FEATURE_MODULES: readonly [];
package/dist/prompts.js CHANGED
@@ -1,6 +1,6 @@
1
- import { APP_MODULES, FEATURE_MODULES, normalizeModuleName } from './cli.js';
1
+ import { isModuleId } from './architecture/modules.js';
2
2
  function hasModuleSelection(args) {
3
- return args.some((arg) => normalizeModuleName(arg.startsWith('--') ? arg.slice(2) : arg) != null);
3
+ return args.some((arg) => arg === '--all' || isModuleId(arg.startsWith('--') ? arg.slice(2) : arg));
4
4
  }
5
5
  export function shouldPromptForInitModules(args, isTTY) {
6
6
  return isTTY && !args.includes('--json') && !hasModuleSelection(args);
@@ -8,12 +8,34 @@ export function shouldPromptForInitModules(args, isTTY) {
8
8
  export function hasUiSelection(args) {
9
9
  return args.some((arg) => arg === '--ui' || arg.startsWith('--ui='));
10
10
  }
11
- export function parsePromptModules(input, fallback) {
11
+ export function parsePromptModuleSelection(input, fallback, all) {
12
12
  const tokens = input
13
13
  .split(/[\s,]+/)
14
- .map((entry) => entry.trim())
14
+ .map((entry) => entry.trim().toLowerCase())
15
15
  .filter(Boolean);
16
- return tokens.length > 0 ? tokens : [...fallback];
16
+ const specialTokens = tokens.filter((token) => token === 'all' || token === 'none');
17
+ if (specialTokens.length > 0 && tokens.length > 1) {
18
+ throw new Error(`"${specialTokens[0]}" must be used by itself.`);
19
+ }
20
+ if (tokens[0] === 'none')
21
+ return [];
22
+ if (tokens[0] === 'all')
23
+ return [...all];
24
+ const selected = tokens.length > 0 ? tokens : [...fallback];
25
+ const allowed = new Set(all);
26
+ const unknown = selected.filter((token) => !allowed.has(token));
27
+ if (unknown.length > 0) {
28
+ throw new Error(`Unknown selection: ${unknown.join(', ')}. Choose from: ${all.join(', ')}, all, or none.`);
29
+ }
30
+ return [...new Set(selected)];
17
31
  }
18
- export const DEFAULT_PROMPT_APP_MODULES = APP_MODULES;
19
- export const DEFAULT_PROMPT_FEATURE_MODULES = FEATURE_MODULES;
32
+ export function parsePromptConfirmation(input) {
33
+ const answer = input.trim().toLowerCase();
34
+ if (answer === '' || answer === 'y' || answer === 'yes')
35
+ return true;
36
+ if (answer === 'n' || answer === 'no')
37
+ return false;
38
+ throw new Error('Enter yes or no.');
39
+ }
40
+ export const DEFAULT_PROMPT_APP_MODULES = ['web'];
41
+ export const DEFAULT_PROMPT_FEATURE_MODULES = [];