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/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,
|
|
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(
|
|
44
|
-
console.log('Select
|
|
45
|
-
const featureAnswer = await rl.question('
|
|
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
|
-
...
|
|
48
|
-
...
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.log(`
|
|
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
|
|
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
|
|
11
|
-
export
|
|
12
|
-
export type
|
|
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
|
|
9
|
-
anhedral init [modules
|
|
10
|
-
anhedral add <module
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
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
|
-
|
|
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');
|
package/dist/dependencies.d.ts
CHANGED
|
@@ -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;
|
package/dist/dependencies.js
CHANGED
|
@@ -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-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
2
|
-
export type {
|
|
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 {
|
|
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
|
+
}
|
package/dist/project.js
ADDED
|
@@ -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
|
|
4
|
-
export declare
|
|
5
|
-
export declare const
|
|
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 {
|
|
1
|
+
import { isModuleId } from './architecture/modules.js';
|
|
2
2
|
function hasModuleSelection(args) {
|
|
3
|
-
return args.some((arg) =>
|
|
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
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
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 = [];
|