anhedral 0.1.10 → 0.4.0

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 (87) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/CONTRIBUTING.md +73 -0
  3. package/LICENSE +196 -17
  4. package/README.md +202 -145
  5. package/SECURITY.md +37 -0
  6. package/SKILL.md +244 -0
  7. package/bin/anhedral.js +1 -1
  8. package/dist/architecture/contributions.d.ts +28 -0
  9. package/dist/architecture/contributions.js +125 -0
  10. package/dist/architecture/index.d.ts +12 -0
  11. package/dist/architecture/index.js +6 -0
  12. package/dist/architecture/manifest.d.ts +53 -0
  13. package/dist/architecture/manifest.js +283 -0
  14. package/dist/architecture/modules.d.ts +37 -0
  15. package/dist/architecture/modules.js +184 -0
  16. package/dist/architecture/plan.d.ts +58 -0
  17. package/dist/architecture/plan.js +145 -0
  18. package/dist/architecture/templates.d.ts +10 -0
  19. package/dist/architecture/templates.js +23 -0
  20. package/dist/bin.d.ts +2 -0
  21. package/dist/bin.js +179 -0
  22. package/dist/cli.d.ts +24 -2
  23. package/dist/cli.js +189 -21
  24. package/dist/composers/root.d.ts +6 -0
  25. package/dist/composers/root.js +35 -0
  26. package/dist/dependencies.d.ts +31 -9
  27. package/dist/dependencies.js +227 -60
  28. package/dist/index.d.ts +8 -2
  29. package/dist/index.js +5 -31
  30. package/dist/print.js +15 -0
  31. package/dist/prompts.d.ts +5 -0
  32. package/dist/prompts.js +19 -0
  33. package/dist/render.d.ts +11 -0
  34. package/dist/render.js +74 -0
  35. package/dist/scaffold.d.ts +64 -12
  36. package/dist/scaffold.js +1726 -1150
  37. package/dist/template-source.d.ts +20 -0
  38. package/dist/template-source.js +180 -0
  39. package/dist/templates/api.d.ts +2 -0
  40. package/dist/templates/api.js +3122 -0
  41. package/dist/templates/assets-private-proxy.d.ts +3 -0
  42. package/dist/templates/assets-private-proxy.js +277 -0
  43. package/dist/templates/desktop.d.ts +2 -0
  44. package/dist/templates/desktop.js +639 -0
  45. package/dist/templates/electron-updater.d.ts +3 -0
  46. package/dist/templates/electron-updater.js +358 -0
  47. package/dist/templates/extension.d.ts +1 -1
  48. package/dist/templates/extension.js +322 -248
  49. package/dist/templates/mobile.d.ts +2 -0
  50. package/dist/templates/mobile.js +720 -0
  51. package/dist/templates/shared.d.ts +2 -0
  52. package/dist/templates/shared.js +520 -0
  53. package/dist/templates/web.d.ts +2 -0
  54. package/dist/templates/web.js +412 -0
  55. package/dist/toolchain.d.ts +0 -8
  56. package/dist/toolchain.js +0 -21
  57. package/dist/transaction.d.ts +22 -0
  58. package/dist/transaction.js +822 -0
  59. package/dist/ui.d.ts +33 -0
  60. package/dist/ui.js +199 -0
  61. package/dist/util.d.ts +1 -5
  62. package/dist/util.js +70 -159
  63. package/dist/version.d.ts +2 -0
  64. package/dist/version.js +2 -0
  65. package/dist/workspace-config.d.ts +11 -0
  66. package/dist/workspace-config.js +372 -0
  67. package/docs/conventions.md +92 -0
  68. package/docs/master-stack-map.md +192 -0
  69. package/docs/output-tree-contract.md +482 -0
  70. package/docs/references/manual-scaffolding.md +403 -0
  71. package/docs/references/provisioning.md +220 -0
  72. package/package.json +57 -13
  73. package/templates/api-fastify/apps/api/tsconfig.json +20 -0
  74. package/templates/catalog.json +23 -0
  75. package/templates/db-drizzle/packages/db/tsconfig.json +14 -0
  76. package/templates/desktop-electron/apps/desktop/tsconfig.json +37 -0
  77. package/templates/extension-wxt/apps/extension/tsconfig.json +41 -0
  78. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +1 -0
  79. package/templates/web-next/apps/web/next-env.d.ts +6 -0
  80. package/dist/branding.d.ts +0 -5
  81. package/dist/branding.js +0 -17
  82. package/dist/commands.d.ts +0 -7
  83. package/dist/commands.js +0 -24
  84. package/dist/templates/backend.d.ts +0 -2
  85. package/dist/templates/backend.js +0 -2115
  86. package/dist/templates/frontend.d.ts +0 -2
  87. package/dist/templates/frontend.js +0 -1268
@@ -0,0 +1,145 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { DEFAULT_MODULE_REGISTRY, isModuleId, resolveModules, } from './modules.js';
3
+ export const FILE_OWNERSHIP_CLASSES = [
4
+ 'managed',
5
+ 'mergeable',
6
+ 'user',
7
+ ];
8
+ export class PlanBuildError extends Error {
9
+ code;
10
+ paths;
11
+ constructor(code, message, paths = []) {
12
+ super(message);
13
+ this.code = code;
14
+ this.paths = paths;
15
+ this.name = 'PlanBuildError';
16
+ }
17
+ }
18
+ const OWNERSHIP_SET = new Set(FILE_OWNERSHIP_CLASSES);
19
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
20
+ function compareStrings(left, right) {
21
+ if (left < right)
22
+ return -1;
23
+ if (left > right)
24
+ return 1;
25
+ return 0;
26
+ }
27
+ export function isFileOwnershipClass(value) {
28
+ return typeof value === 'string' && OWNERSHIP_SET.has(value);
29
+ }
30
+ export function isSha256(value) {
31
+ return typeof value === 'string' && SHA256_PATTERN.test(value);
32
+ }
33
+ /**
34
+ * Hash rendered text or exact filesystem bytes.
35
+ *
36
+ * Strings are encoded as UTF-8 for deterministic generation-plan fingerprints.
37
+ * Callers hashing an existing file must pass its Buffer/Uint8Array so malformed
38
+ * byte sequences cannot collapse to the same Unicode replacement characters.
39
+ */
40
+ export function hashContent(content) {
41
+ const hash = createHash('sha256');
42
+ if (typeof content === 'string')
43
+ hash.update(content, 'utf8');
44
+ else
45
+ hash.update(content);
46
+ return hash.digest('hex');
47
+ }
48
+ export function writePolicyForOwnership(ownership) {
49
+ switch (ownership) {
50
+ case 'managed': return 'replace-if-unmodified';
51
+ case 'mergeable': return 'structural-merge';
52
+ case 'user': return 'create-only';
53
+ }
54
+ }
55
+ export function normalizePlanPath(value) {
56
+ if (typeof value !== 'string') {
57
+ throw new PlanBuildError('INVALID_PLAN_PATH', `Plan paths must be strings: ${String(value)}`, Object.freeze([String(value)]));
58
+ }
59
+ const pathValue = value.trim();
60
+ if (pathValue.length === 0
61
+ || pathValue !== value
62
+ || pathValue === '.'
63
+ || pathValue.startsWith('/')
64
+ || /^[a-zA-Z]:\//.test(pathValue)
65
+ || pathValue.includes('\\')
66
+ || pathValue.includes('\0')) {
67
+ throw new PlanBuildError('INVALID_PLAN_PATH', `Plan paths must be non-empty relative POSIX paths: ${JSON.stringify(value)}`, Object.freeze([value]));
68
+ }
69
+ const segments = pathValue.split('/');
70
+ if (segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
71
+ throw new PlanBuildError('INVALID_PLAN_PATH', `Plan paths cannot contain empty, current-directory, or parent-directory segments: ${JSON.stringify(value)}`, Object.freeze([value]));
72
+ }
73
+ return segments.join('/');
74
+ }
75
+ function normalizeOwner(value) {
76
+ if (value === 'root' || isModuleId(value))
77
+ return value;
78
+ throw new PlanBuildError('INVALID_FILE_OWNER', `Unknown plan contribution owner: ${String(value)}`);
79
+ }
80
+ function canonicalFingerprintPayload(operation, requestedModules, resolvedModules, dependencyAddedModules, dependencyEdges, files) {
81
+ return JSON.stringify({
82
+ planVersion: 1,
83
+ operation,
84
+ requestedModules,
85
+ resolvedModules,
86
+ dependencyAddedModules,
87
+ dependencyEdges,
88
+ files: files.map((file) => ({
89
+ path: file.path,
90
+ owner: file.owner,
91
+ ownership: file.ownership,
92
+ writePolicy: file.writePolicy,
93
+ contentHash: file.contentHash,
94
+ })),
95
+ });
96
+ }
97
+ export function buildGenerationPlan(input) {
98
+ if (!['init', 'add'].includes(input.operation)) {
99
+ throw new PlanBuildError('INVALID_OPERATION', `Unknown generation operation: ${String(input.operation)}`);
100
+ }
101
+ const registry = input.registry ?? DEFAULT_MODULE_REGISTRY;
102
+ const resolution = resolveModules(input.requestedModules, registry);
103
+ const resolvedSet = new Set(resolution.resolvedModules);
104
+ const plannedByPath = new Map();
105
+ for (const contribution of input.contributions ?? []) {
106
+ const owner = normalizeOwner(contribution.module);
107
+ if (owner !== 'root' && !resolvedSet.has(owner)) {
108
+ throw new PlanBuildError('UNRESOLVED_CONTRIBUTION', `Module ${owner} contributed files but is not in the resolved module plan`);
109
+ }
110
+ for (const file of contribution.files) {
111
+ const filePath = normalizePlanPath(file.path);
112
+ if (typeof file.content !== 'string') {
113
+ throw new PlanBuildError('INVALID_FILE_CONTENT', `Planned file content must be a string: ${filePath}`, Object.freeze([filePath]));
114
+ }
115
+ if (!isFileOwnershipClass(file.ownership)) {
116
+ throw new PlanBuildError('INVALID_FILE_OWNERSHIP', `Unknown ownership class for ${filePath}: ${String(file.ownership)}`, Object.freeze([filePath]));
117
+ }
118
+ const existing = plannedByPath.get(filePath);
119
+ if (existing) {
120
+ throw new PlanBuildError('DUPLICATE_OUTPUT_PATH', `Output path ${filePath} is owned by both ${existing.owner} and ${owner}; compose it before planning`, Object.freeze([filePath]));
121
+ }
122
+ const plannedFile = Object.freeze({
123
+ path: filePath,
124
+ owner,
125
+ ownership: file.ownership,
126
+ writePolicy: writePolicyForOwnership(file.ownership),
127
+ content: file.content,
128
+ contentHash: hashContent(file.content),
129
+ });
130
+ plannedByPath.set(filePath, plannedFile);
131
+ }
132
+ }
133
+ const files = Object.freeze([...plannedByPath.values()].sort((left, right) => compareStrings(left.path, right.path)));
134
+ const fingerprint = hashContent(canonicalFingerprintPayload(input.operation, resolution.requestedModules, resolution.resolvedModules, resolution.dependencyAddedModules, resolution.dependencyEdges, files));
135
+ return Object.freeze({
136
+ planVersion: 1,
137
+ operation: input.operation,
138
+ requestedModules: resolution.requestedModules,
139
+ resolvedModules: resolution.resolvedModules,
140
+ dependencyAddedModules: resolution.dependencyAddedModules,
141
+ dependencyEdges: resolution.dependencyEdges,
142
+ files,
143
+ fingerprint,
144
+ });
145
+ }
@@ -0,0 +1,10 @@
1
+ import type { ModuleId } from './modules.js';
2
+ export declare const TEMPLATE_IDS: readonly ["web-next", "mobile-expo", "api-fastify", "desktop-electron", "extension-wxt", "db-drizzle"];
3
+ export type TemplateId = (typeof TEMPLATE_IDS)[number];
4
+ export type TemplateProvenance = {
5
+ readonly version: number;
6
+ readonly sha256: string;
7
+ };
8
+ export type TemplateProvenanceMap = Readonly<Partial<Record<TemplateId, TemplateProvenance>>>;
9
+ export declare function isTemplateId(value: unknown): value is TemplateId;
10
+ export declare function templateIdsForModules(modules: readonly ModuleId[]): readonly TemplateId[];
@@ -0,0 +1,23 @@
1
+ export const TEMPLATE_IDS = [
2
+ 'web-next',
3
+ 'mobile-expo',
4
+ 'api-fastify',
5
+ 'desktop-electron',
6
+ 'extension-wxt',
7
+ 'db-drizzle',
8
+ ];
9
+ const TEMPLATE_ID_SET = new Set(TEMPLATE_IDS);
10
+ export function isTemplateId(value) {
11
+ return typeof value === 'string' && TEMPLATE_ID_SET.has(value);
12
+ }
13
+ export function templateIdsForModules(modules) {
14
+ const selected = new Set(modules);
15
+ return Object.freeze([
16
+ selected.has('web') ? 'web-next' : null,
17
+ selected.has('mobile') ? 'mobile-expo' : null,
18
+ selected.has('api') ? 'api-fastify' : null,
19
+ selected.has('desktop') ? 'desktop-electron' : null,
20
+ selected.has('extension') ? 'extension-wxt' : null,
21
+ selected.has('db') ? 'db-drizzle' : null,
22
+ ].filter((id) => id !== null));
23
+ }
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { argv, stdin, stdout } from 'node:process';
4
+ import { buildAddOptions, buildOptions, buildOptionsForRoot, parseCli, parseNewProjectRequest, parseUiAddOptions, USAGE } from './cli.js';
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';
7
+ import { GENERATOR_VERSION } from './version.js';
8
+ import { PostCommitError } from './transaction.js';
9
+ function writeCliError(error, code, json) {
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ if (json)
12
+ console.error(JSON.stringify({ error: message, code }));
13
+ else
14
+ console.error('Error:', message);
15
+ }
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
+ async function promptForInitModules(args) {
38
+ if (!shouldPromptForInitModules(args, stdin.isTTY === true))
39
+ return args;
40
+ const rl = createInterface({ input: stdin, output: stdout });
41
+ 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]: ');
46
+ const selected = [
47
+ ...parsePromptModules(appAnswer, DEFAULT_PROMPT_APP_MODULES),
48
+ ...parsePromptModules(featureAnswer, DEFAULT_PROMPT_FEATURE_MODULES),
49
+ ];
50
+ const result = [...args, ...selected.map((moduleName) => `--${moduleName}`)];
51
+ const hasUiClient = selected.some((moduleName) => ['web', 'mobile', 'desktop', 'extension'].includes(moduleName));
52
+ if (hasUiClient && !hasUiSelection(args)) {
53
+ console.log('Optionally add starter UI components to each selected client.');
54
+ const componentAnswer = await rl.question('Starter components [none]: ');
55
+ if (componentAnswer.trim())
56
+ result.push(`--ui=${componentAnswer}`);
57
+ }
58
+ if (selected.includes('mobile') && !args.some((arg) => arg.startsWith('--native-styling'))) {
59
+ const stylingAnswer = (await rl.question('Expo styling [nativewind]: ')).trim();
60
+ result.push(`--native-styling=${stylingAnswer || 'nativewind'}`);
61
+ }
62
+ return result;
63
+ }
64
+ finally {
65
+ rl.close();
66
+ }
67
+ }
68
+ async function main() {
69
+ const args = argv.slice(2);
70
+ const command = args[0];
71
+ const rawArgs = args.slice(1);
72
+ const json = rawArgs.includes('--json');
73
+ const verbose = rawArgs.includes('--verbose');
74
+ if (json)
75
+ process.env.ANHEDRAL_QUIET = '1';
76
+ if (verbose)
77
+ process.env.ANHEDRAL_VERBOSE = '1';
78
+ if (command === '--version' || command === '-v') {
79
+ if (json)
80
+ console.log(JSON.stringify({ version: GENERATOR_VERSION }));
81
+ else
82
+ console.log(GENERATOR_VERSION);
83
+ return;
84
+ }
85
+ if (!command || command === '--help' || command === '-h') {
86
+ if (json)
87
+ console.log(JSON.stringify({ usage: USAGE }));
88
+ else
89
+ console.log(USAGE);
90
+ return;
91
+ }
92
+ if (!['new', 'init', 'add', 'ui', 'upgrade', 'doctor'].includes(command)) {
93
+ if (json)
94
+ writeCliError(new Error(`Unknown command: ${command}`), 'UNKNOWN_COMMAND', true);
95
+ else {
96
+ console.error(`Unknown command: ${command}`);
97
+ console.log(USAGE);
98
+ }
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
103
+ if (json)
104
+ console.log(JSON.stringify({ usage: USAGE }));
105
+ else
106
+ console.log(USAGE);
107
+ return;
108
+ }
109
+ let phase = 'arguments';
110
+ 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.`);
119
+ }
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}`);
133
+ }
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
+ }
166
+ }
167
+ catch (error) {
168
+ const code = error instanceof PostCommitError
169
+ ? 'POST_COMMIT_FAILED'
170
+ : phase === 'arguments'
171
+ ? 'INVALID_ARGUMENT'
172
+ : command === 'doctor'
173
+ ? 'DOCTOR_FAILED'
174
+ : 'GENERATION_FAILED';
175
+ writeCliError(error, code, json);
176
+ process.exitCode = 1;
177
+ }
178
+ }
179
+ void main();
package/dist/cli.d.ts CHANGED
@@ -1,10 +1,32 @@
1
- import type { InitOptions } from './scaffold.js';
2
- export declare const USAGE = "\nanhedral init [--toolchain <latest|stable>] [--skip-install]\n\nCommands:\n anhedral init\n Expo + React Native Reusables, Fastify, WXT extension, Neon + Drizzle, Cloudflare R2/CDN, Clerk, RevenueCat + Stripe, Vercel\n";
1
+ import type { AddOptions, InitOptions } from './scaffold.js';
2
+ import { type NativeStylingLibrary } from './ui.js';
3
+ 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 type NewProjectRequest = {
6
+ readonly directory: string;
7
+ readonly moduleArgs: readonly string[];
8
+ };
9
+ 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;
3
15
  export type ParsedFlags = {
4
16
  toolchain?: string;
5
17
  skipInstall?: boolean;
18
+ dryRun?: boolean;
19
+ json?: boolean;
20
+ verbose?: boolean;
21
+ uiComponents: string[];
22
+ nativeStyling?: NativeStylingLibrary;
23
+ modules: Set<SupportedModule>;
6
24
  };
7
25
  export declare function parseCli(args: string[]): ParsedFlags;
26
+ export declare function normalizeModuleName(raw: string): SupportedModule | null;
8
27
  export declare function deriveProjectName(cwd: string): string;
9
28
  export declare function deriveDisplayName(cwd: string): string;
10
29
  export declare function buildOptions(flags: ParsedFlags): InitOptions;
30
+ export declare function buildOptionsForRoot(flags: ParsedFlags, root: string): InitOptions;
31
+ export declare function parseUiAddOptions(args: readonly string[]): UiAddOptions;
32
+ export declare function buildAddOptions(modules: string[], flags: ParsedFlags): AddOptions;
package/dist/cli.js CHANGED
@@ -1,24 +1,95 @@
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';
5
+ import { packageNameFromText } from './render.js';
6
+ import { isNativeStylingLibrary, isUiTarget, parseUiComponentList, } from './ui.js';
4
7
  export const USAGE = `
5
- anhedral init [--toolchain <latest|stable>] [--skip-install]
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]
11
+ anhedral ui add <component...> [--target <client>] [--skip-install] [--dry-run] [--json] [--verbose]
12
+ anhedral upgrade [--skip-install] [--dry-run] [--json] [--verbose]
13
+ anhedral doctor [--json] [--verbose]
14
+ anhedral --version
6
15
 
7
16
  Commands:
8
- anhedral init
9
- Expo + React Native Reusables, Fastify, WXT extension, Neon + Drizzle, Cloudflare R2/CDN, Clerk, RevenueCat + Stripe, Vercel
17
+ anhedral new my-app
18
+ Generate the complete TypeScript stack in a new directory. With no module flags, every module is included.
19
+ anhedral new my-app --web --api --db --auth
20
+ Generate a web app, Fastify API, shared database package, and auth wiring.
21
+ anhedral init --web --api --db --auth
22
+ Generate the same readable workspace in the current empty directory.
23
+ anhedral add mobile extension
24
+ Add missing modules to an existing Anhedral project.
25
+ anhedral ui add button dialog --target mobile
26
+ Add React Native Reusables components to Expo. DOM clients use shadcn/ui.
27
+ anhedral upgrade
28
+ Transactionally upgrade a supported older Anhedral project before adding modules.
10
29
  `;
30
+ export function parseNewProjectRequest(args) {
31
+ const [directory, ...moduleArgs] = args;
32
+ if (!directory || directory.startsWith('--'))
33
+ throw new Error('anhedral new requires a destination directory before module flags');
34
+ return Object.freeze({ directory, moduleArgs: Object.freeze(moduleArgs) });
35
+ }
36
+ export const APP_MODULES = ['web', 'mobile', 'api', 'desktop', 'extension'];
37
+ export const FEATURE_MODULES = ['db', 'auth', 'billing', 'storage', 'native-subscriptions', 'electron-updater'];
11
38
  export function parseCli(args) {
12
- const flags = {};
39
+ const flags = { modules: new Set(), uiComponents: [] };
13
40
  for (let index = 0; index < args.length; index += 1) {
14
41
  const token = args[index];
15
42
  if (!token.startsWith('--')) {
16
- throw new Error(`Unexpected argument: ${token}. Use flags: --toolchain, --skip-install`);
43
+ const moduleName = normalizeModuleName(token);
44
+ if (moduleName) {
45
+ flags.modules.add(moduleName);
46
+ continue;
47
+ }
48
+ throw new Error(`Unexpected argument: ${token}. Use module names, module flags, --toolchain, or --skip-install`);
17
49
  }
18
50
  if (token === '--skip-install') {
19
51
  flags.skipInstall = true;
20
52
  continue;
21
53
  }
54
+ if (token === '--dry-run') {
55
+ flags.dryRun = true;
56
+ continue;
57
+ }
58
+ if (token === '--json') {
59
+ flags.json = true;
60
+ continue;
61
+ }
62
+ if (token === '--verbose') {
63
+ flags.verbose = true;
64
+ continue;
65
+ }
66
+ if (token === '--ui') {
67
+ const value = args[index + 1];
68
+ if (!value || value.startsWith('--'))
69
+ throw new Error('Missing value for --ui');
70
+ flags.uiComponents.push(...parseUiComponentList(value));
71
+ index += 1;
72
+ continue;
73
+ }
74
+ if (token.startsWith('--ui=')) {
75
+ flags.uiComponents.push(...parseUiComponentList(token.slice('--ui='.length)));
76
+ continue;
77
+ }
78
+ if (token === '--native-styling') {
79
+ const value = args[index + 1];
80
+ if (!isNativeStylingLibrary(value))
81
+ throw new Error('--native-styling must be nativewind or uniwind');
82
+ flags.nativeStyling = value;
83
+ index += 1;
84
+ continue;
85
+ }
86
+ if (token.startsWith('--native-styling=')) {
87
+ const value = token.slice('--native-styling='.length);
88
+ if (!isNativeStylingLibrary(value))
89
+ throw new Error('--native-styling must be nativewind or uniwind');
90
+ flags.nativeStyling = value;
91
+ continue;
92
+ }
22
93
  if (token === '--toolchain') {
23
94
  const value = args[index + 1];
24
95
  if (!value || value.startsWith('--')) {
@@ -28,40 +99,137 @@ export function parseCli(args) {
28
99
  index += 1;
29
100
  continue;
30
101
  }
102
+ if (token.startsWith('--toolchain=')) {
103
+ const value = token.slice('--toolchain='.length);
104
+ if (!value) {
105
+ throw new Error('Missing value for --toolchain');
106
+ }
107
+ flags.toolchain = value;
108
+ continue;
109
+ }
110
+ const moduleName = normalizeModuleName(token.slice(2));
111
+ if (moduleName) {
112
+ flags.modules.add(moduleName);
113
+ continue;
114
+ }
31
115
  throw new Error(`Unknown flag: ${token}`);
32
116
  }
33
117
  return flags;
34
118
  }
119
+ 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;
125
+ }
35
126
  export function deriveProjectName(cwd) {
36
- const base = path.basename(cwd);
37
- const sanitized = base
38
- .trim()
39
- .toLowerCase()
40
- .replace(/[^a-z0-9_-]+/g, '-')
41
- .replace(/^-+|-+$/g, '');
42
- return sanitized || 'anhedral-app';
127
+ return packageNameFromText(path.basename(cwd));
43
128
  }
44
129
  export function deriveDisplayName(cwd) {
45
130
  const base = path.basename(cwd).trim();
46
131
  return base || 'Anhedral App';
47
132
  }
48
133
  export function buildOptions(flags) {
49
- const cwd = process.cwd();
50
- const projectName = deriveProjectName(cwd);
51
- const displayName = deriveDisplayName(cwd);
134
+ return buildOptionsForRoot(flags, process.cwd());
135
+ }
136
+ export function buildOptionsForRoot(flags, root) {
137
+ const resolvedRoot = path.resolve(root);
138
+ const projectName = deriveProjectName(resolvedRoot);
139
+ const displayName = deriveDisplayName(resolvedRoot);
52
140
  if (flags.toolchain != null && !TOOLCHAIN_CHANNELS.includes(flags.toolchain)) {
53
141
  throw new Error(`--toolchain must be one of: ${TOOLCHAIN_CHANNELS.join(', ')}`);
54
142
  }
143
+ const requestedModules = flags.modules.size === 0
144
+ ? [...APP_MODULES, ...FEATURE_MODULES]
145
+ : [...flags.modules];
146
+ const resolution = resolveModules(requestedModules);
55
147
  return {
56
148
  projectName,
57
149
  displayName,
58
- auth: 'clerk',
59
- payments: 'revenuecat_stripe',
60
- db: 'neon',
61
- orm: 'drizzle',
62
- storage: 'r2',
63
- api: 'fastify',
150
+ modules: [...resolution.requestedModules],
151
+ uiComponents: Array.from(new Set(flags.uiComponents)),
152
+ nativeStyling: flags.nativeStyling ?? 'nativewind',
64
153
  skipInstall: flags.skipInstall === true || env.ANHEDRAL_SKIP_INSTALL === '1',
154
+ dryRun: flags.dryRun === true,
155
+ json: flags.json === true,
65
156
  toolchainChannel: resolveToolchainChannel(flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN),
157
+ rootDirectory: resolvedRoot,
158
+ };
159
+ }
160
+ export function parseUiAddOptions(args) {
161
+ const components = [];
162
+ const targets = [];
163
+ let skipInstall = env.ANHEDRAL_SKIP_INSTALL === '1';
164
+ let dryRun = false;
165
+ let json = false;
166
+ for (let index = 0; index < args.length; index += 1) {
167
+ const token = args[index];
168
+ if (!token.startsWith('--')) {
169
+ components.push(...parseUiComponentList(token));
170
+ continue;
171
+ }
172
+ if (token === '--target') {
173
+ const value = args[index + 1];
174
+ if (!isUiTarget(value))
175
+ throw new Error(`--target must be one of: web, mobile, desktop, extension`);
176
+ targets.push(value);
177
+ index += 1;
178
+ continue;
179
+ }
180
+ if (token.startsWith('--target=')) {
181
+ const value = token.slice('--target='.length);
182
+ if (!isUiTarget(value))
183
+ throw new Error(`--target must be one of: web, mobile, desktop, extension`);
184
+ targets.push(value);
185
+ continue;
186
+ }
187
+ if (token === '--skip-install') {
188
+ skipInstall = true;
189
+ continue;
190
+ }
191
+ if (token === '--dry-run') {
192
+ dryRun = true;
193
+ continue;
194
+ }
195
+ if (token === '--json') {
196
+ json = true;
197
+ continue;
198
+ }
199
+ if (token === '--verbose') {
200
+ continue;
201
+ }
202
+ throw new Error(`Unknown UI option: ${token}`);
203
+ }
204
+ if (components.length === 0)
205
+ throw new Error('anhedral ui add requires at least one component');
206
+ return {
207
+ components: Array.from(new Set(components)),
208
+ targets: Array.from(new Set(targets)),
209
+ skipInstall,
210
+ dryRun,
211
+ json,
212
+ };
213
+ }
214
+ export function buildAddOptions(modules, flags) {
215
+ const requestedModules = [...modules, ...flags.modules];
216
+ if (requestedModules.length === 0) {
217
+ throw new Error('anhedral add requires at least one module');
218
+ }
219
+ const normalizedModules = requestedModules.map((moduleName) => {
220
+ const normalized = normalizeModuleName(moduleName);
221
+ if (!normalized) {
222
+ throw new Error(`Unknown module: ${moduleName}`);
223
+ }
224
+ return normalized;
225
+ });
226
+ return {
227
+ modules: Array.from(new Set(normalizedModules)),
228
+ skipInstall: flags.skipInstall === true || env.ANHEDRAL_SKIP_INSTALL === '1',
229
+ dryRun: flags.dryRun === true,
230
+ json: flags.json === true,
231
+ ...((flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN) != null
232
+ ? { toolchainChannel: resolveToolchainChannel(flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN) }
233
+ : {}),
66
234
  };
67
235
  }
@@ -0,0 +1,6 @@
1
+ import type { CompositionModel } from '../architecture/contributions.js';
2
+ export type RootEnvironmentContext = {
3
+ readonly corsOrigins: readonly string[];
4
+ };
5
+ export declare function composeRootEnvironment(model: CompositionModel, context: RootEnvironmentContext): readonly string[];
6
+ export declare function composeVercelCrons(model: CompositionModel): readonly Record<string, string>[];