anhedral 0.4.2 → 0.6.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 (89) hide show
  1. package/README.md +29 -269
  2. package/assets/anhedral-cli-init-technical.svg +322 -0
  3. package/assets/anhedral-cli-init.svg +257 -173
  4. package/dist/bin.js +28 -191
  5. package/dist/dependencies.d.ts +7 -57
  6. package/dist/dependencies.js +96 -308
  7. package/dist/index.d.ts +2 -7
  8. package/dist/index.js +1 -4
  9. package/dist/platforms/desktop.js +401 -0
  10. package/dist/platforms/extension.js +401 -0
  11. package/dist/platforms/mobile.js +232 -0
  12. package/dist/project.d.ts +0 -26
  13. package/dist/standard-products.d.ts +2 -0
  14. package/dist/standard-products.js +6 -0
  15. package/dist/standard.d.ts +18 -0
  16. package/dist/standard.js +578 -0
  17. package/dist/version.d.ts +1 -1
  18. package/dist/version.js +1 -1
  19. package/docs/application-stack-standard.md +700 -0
  20. package/package.json +34 -51
  21. package/CHANGELOG.md +0 -178
  22. package/CONTRIBUTING.md +0 -77
  23. package/SECURITY.md +0 -37
  24. package/SKILL.md +0 -339
  25. package/assets/images/svg/ably-symbol-color.svg +0 -8
  26. package/bin/anhedral.js +0 -2
  27. package/dist/architecture/contributions.d.ts +0 -28
  28. package/dist/architecture/contributions.js +0 -125
  29. package/dist/architecture/index.d.ts +0 -12
  30. package/dist/architecture/index.js +0 -6
  31. package/dist/architecture/manifest.d.ts +0 -53
  32. package/dist/architecture/manifest.js +0 -283
  33. package/dist/architecture/modules.d.ts +0 -41
  34. package/dist/architecture/modules.js +0 -187
  35. package/dist/architecture/plan.d.ts +0 -58
  36. package/dist/architecture/plan.js +0 -145
  37. package/dist/architecture/templates.d.ts +0 -10
  38. package/dist/architecture/templates.js +0 -23
  39. package/dist/cli.d.ts +0 -32
  40. package/dist/cli.js +0 -281
  41. package/dist/composers/root.d.ts +0 -6
  42. package/dist/composers/root.js +0 -35
  43. package/dist/print.d.ts +0 -8
  44. package/dist/print.js +0 -48
  45. package/dist/prompts.d.ts +0 -6
  46. package/dist/prompts.js +0 -41
  47. package/dist/scaffold.d.ts +0 -64
  48. package/dist/scaffold.js +0 -2102
  49. package/dist/template-source.d.ts +0 -20
  50. package/dist/template-source.js +0 -180
  51. package/dist/templates/api.d.ts +0 -2
  52. package/dist/templates/api.js +0 -3187
  53. package/dist/templates/assets-private-proxy.d.ts +0 -3
  54. package/dist/templates/assets-private-proxy.js +0 -277
  55. package/dist/templates/desktop.js +0 -792
  56. package/dist/templates/electron-updater.d.ts +0 -3
  57. package/dist/templates/electron-updater.js +0 -358
  58. package/dist/templates/extension.js +0 -856
  59. package/dist/templates/first-run.d.ts +0 -2
  60. package/dist/templates/first-run.js +0 -167
  61. package/dist/templates/mobile.js +0 -861
  62. package/dist/templates/shared.d.ts +0 -2
  63. package/dist/templates/shared.js +0 -577
  64. package/dist/templates/web.d.ts +0 -2
  65. package/dist/templates/web.js +0 -569
  66. package/dist/toolchain.d.ts +0 -3
  67. package/dist/toolchain.js +0 -8
  68. package/dist/ui.d.ts +0 -33
  69. package/dist/ui.js +0 -199
  70. package/dist/workspace-config.d.ts +0 -11
  71. package/dist/workspace-config.js +0 -372
  72. package/docs/NORTHSTAR.md +0 -114
  73. package/docs/conventions.md +0 -94
  74. package/docs/engineering-principles.md +0 -88
  75. package/docs/master-stack-map.md +0 -199
  76. package/docs/output-tree-contract.md +0 -493
  77. package/docs/references/manual-scaffolding.md +0 -421
  78. package/docs/references/provisioning.md +0 -577
  79. package/scripts/render-master-stack-map.mjs +0 -510
  80. package/templates/api-fastify/apps/api/tsconfig.json +0 -20
  81. package/templates/catalog.json +0 -23
  82. package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
  83. package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
  84. package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
  85. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
  86. package/templates/web-next/apps/web/next-env.d.ts +0 -6
  87. /package/dist/{templates → platforms}/desktop.d.ts +0 -0
  88. /package/dist/{templates → platforms}/extension.d.ts +0 -0
  89. /package/dist/{templates → platforms}/mobile.d.ts +0 -0
@@ -1,145 +0,0 @@
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
- }
@@ -1,10 +0,0 @@
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[];
@@ -1,23 +0,0 @@
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/cli.d.ts DELETED
@@ -1,32 +0,0 @@
1
- import type { AddOptions, InitOptions } from './scaffold.js';
2
- import { APP_MODULES, FEATURE_MODULES, type AppModule, type FeatureModule, type ModuleId } from './architecture/modules.js';
3
- import { type NativeStylingLibrary } from './ui.js';
4
- import type { UiAddOptions } from './scaffold.js';
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";
6
- export type NewProjectRequest = {
7
- readonly directory: string;
8
- readonly moduleArgs: readonly string[];
9
- };
10
- export declare function parseNewProjectRequest(args: readonly string[]): NewProjectRequest;
11
- export { APP_MODULES, FEATURE_MODULES };
12
- export type { AppModule, FeatureModule };
13
- export type SupportedModule = ModuleId;
14
- export type ParsedFlags = {
15
- toolchain?: string;
16
- skipInstall?: boolean;
17
- dryRun?: boolean;
18
- json?: boolean;
19
- verbose?: boolean;
20
- initializeGit?: boolean;
21
- uiComponents: string[];
22
- nativeStyling?: NativeStylingLibrary;
23
- modules: Set<SupportedModule>;
24
- };
25
- export declare function parseCli(args: readonly string[]): ParsedFlags;
26
- export declare function normalizeModuleName(raw: string): SupportedModule | null;
27
- export declare function deriveProjectName(cwd: string): string;
28
- export declare function deriveDisplayName(cwd: string): string;
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 DELETED
@@ -1,281 +0,0 @@
1
- import path from 'node:path';
2
- import { env } from 'node:process';
3
- import { TOOLCHAIN_CHANNELS, resolveToolchainChannel } from './toolchain.js';
4
- import { APP_MODULES, FEATURE_MODULES, isModuleId, resolveModules, } from './architecture/modules.js';
5
- import { packageNameFromText } from './render.js';
6
- import { isNativeStylingLibrary, isUiTarget, parseUiComponentList, } from './ui.js';
7
- export const USAGE = `
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
- 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
15
-
16
- Commands:
17
- anhedral new my-app
18
- Interactively choose a focused stack in a terminal. In noninteractive use, pass module flags; no flags explicitly means the complete stack.
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.
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.
51
- `;
52
- export function parseNewProjectRequest(args) {
53
- const [directory, ...moduleArgs] = args;
54
- if (!directory || directory.startsWith('--'))
55
- throw new Error('anhedral new requires a destination directory before module flags');
56
- return Object.freeze({ directory, moduleArgs: Object.freeze(moduleArgs) });
57
- }
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
- }
65
- export function parseCli(args) {
66
- const flags = { modules: new Set(), uiComponents: [] };
67
- for (let index = 0; index < args.length; index += 1) {
68
- const token = args[index];
69
- if (!token.startsWith('--')) {
70
- const moduleName = normalizeModuleName(token);
71
- if (moduleName) {
72
- flags.modules.add(moduleName);
73
- continue;
74
- }
75
- throw new Error(`Unexpected argument: ${token}. Use module names, module flags, --toolchain, or --skip-install`);
76
- }
77
- if (token === '--skip-install') {
78
- flags.skipInstall = true;
79
- continue;
80
- }
81
- if (token === '--dry-run') {
82
- flags.dryRun = true;
83
- continue;
84
- }
85
- if (token === '--json') {
86
- flags.json = true;
87
- continue;
88
- }
89
- if (token === '--verbose') {
90
- flags.verbose = true;
91
- continue;
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
- }
106
- if (token === '--ui') {
107
- const value = args[index + 1];
108
- if (!value || value.startsWith('--'))
109
- throw new Error('Missing value for --ui');
110
- flags.uiComponents.push(...parseUiComponentList(value));
111
- index += 1;
112
- continue;
113
- }
114
- if (token.startsWith('--ui=')) {
115
- flags.uiComponents.push(...parseUiComponentList(token.slice('--ui='.length)));
116
- continue;
117
- }
118
- if (token === '--native-styling') {
119
- const value = args[index + 1];
120
- if (!isNativeStylingLibrary(value))
121
- throw new Error('--native-styling must be nativewind or uniwind');
122
- flags.nativeStyling = assignOption('--native-styling', flags.nativeStyling, value);
123
- index += 1;
124
- continue;
125
- }
126
- if (token.startsWith('--native-styling=')) {
127
- const value = token.slice('--native-styling='.length);
128
- if (!isNativeStylingLibrary(value))
129
- throw new Error('--native-styling must be nativewind or uniwind');
130
- flags.nativeStyling = assignOption('--native-styling', flags.nativeStyling, value);
131
- continue;
132
- }
133
- if (token === '--toolchain') {
134
- const value = args[index + 1];
135
- if (!value || value.startsWith('--')) {
136
- throw new Error('Missing value for --toolchain');
137
- }
138
- flags.toolchain = assignOption('--toolchain', flags.toolchain, value);
139
- index += 1;
140
- continue;
141
- }
142
- if (token.startsWith('--toolchain=')) {
143
- const value = token.slice('--toolchain='.length);
144
- if (!value) {
145
- throw new Error('Missing value for --toolchain');
146
- }
147
- flags.toolchain = assignOption('--toolchain', flags.toolchain, value);
148
- continue;
149
- }
150
- const moduleName = normalizeModuleName(token.slice(2));
151
- if (moduleName) {
152
- flags.modules.add(moduleName);
153
- continue;
154
- }
155
- throw new Error(`Unknown flag: ${token}`);
156
- }
157
- return flags;
158
- }
159
- export function normalizeModuleName(raw) {
160
- return isModuleId(raw) ? raw : null;
161
- }
162
- export function deriveProjectName(cwd) {
163
- return packageNameFromText(path.basename(cwd));
164
- }
165
- export function deriveDisplayName(cwd) {
166
- const base = path.basename(cwd).trim();
167
- return base || 'Anhedral App';
168
- }
169
- export function buildOptions(flags) {
170
- return buildOptionsForRoot(flags, process.cwd());
171
- }
172
- export function buildOptionsForRoot(flags, root) {
173
- const resolvedRoot = path.resolve(root);
174
- const projectName = deriveProjectName(resolvedRoot);
175
- const displayName = deriveDisplayName(resolvedRoot);
176
- if (flags.toolchain != null && !TOOLCHAIN_CHANNELS.includes(flags.toolchain)) {
177
- throw new Error(`--toolchain must be one of: ${TOOLCHAIN_CHANNELS.join(', ')}`);
178
- }
179
- const requestedModules = flags.modules.size === 0
180
- ? [...APP_MODULES, ...FEATURE_MODULES]
181
- : [...flags.modules];
182
- const resolution = resolveModules(requestedModules);
183
- return {
184
- projectName,
185
- displayName,
186
- modules: [...resolution.requestedModules],
187
- uiComponents: Array.from(new Set(flags.uiComponents)),
188
- nativeStyling: flags.nativeStyling ?? 'nativewind',
189
- skipInstall: flags.skipInstall === true || env.ANHEDRAL_SKIP_INSTALL === '1',
190
- dryRun: flags.dryRun === true,
191
- json: flags.json === true,
192
- toolchainChannel: resolveToolchainChannel(flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN),
193
- rootDirectory: resolvedRoot,
194
- initializeGit: flags.initializeGit,
195
- };
196
- }
197
- export function parseUiAddOptions(args) {
198
- const components = [];
199
- const targets = [];
200
- let skipInstall = env.ANHEDRAL_SKIP_INSTALL === '1';
201
- let dryRun = false;
202
- let json = false;
203
- for (let index = 0; index < args.length; index += 1) {
204
- const token = args[index];
205
- if (!token.startsWith('--')) {
206
- components.push(...parseUiComponentList(token));
207
- continue;
208
- }
209
- if (token === '--target') {
210
- const value = args[index + 1];
211
- if (!isUiTarget(value))
212
- throw new Error(`--target must be one of: web, mobile, desktop, extension`);
213
- targets.push(value);
214
- index += 1;
215
- continue;
216
- }
217
- if (token.startsWith('--target=')) {
218
- const value = token.slice('--target='.length);
219
- if (!isUiTarget(value))
220
- throw new Error(`--target must be one of: web, mobile, desktop, extension`);
221
- targets.push(value);
222
- continue;
223
- }
224
- if (token === '--skip-install') {
225
- skipInstall = true;
226
- continue;
227
- }
228
- if (token === '--dry-run') {
229
- dryRun = true;
230
- continue;
231
- }
232
- if (token === '--json') {
233
- json = true;
234
- continue;
235
- }
236
- if (token === '--verbose') {
237
- continue;
238
- }
239
- throw new Error(`Unknown UI option: ${token}`);
240
- }
241
- if (components.length === 0)
242
- throw new Error('anhedral ui add requires at least one component');
243
- return {
244
- components: Array.from(new Set(components)),
245
- targets: Array.from(new Set(targets)),
246
- skipInstall,
247
- dryRun,
248
- json,
249
- };
250
- }
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
- }
261
- const requestedModules = [...modules, ...flags.modules];
262
- if (requestedModules.length === 0) {
263
- throw new Error('anhedral add requires at least one module');
264
- }
265
- const normalizedModules = requestedModules.map((moduleName) => {
266
- const normalized = normalizeModuleName(moduleName);
267
- if (!normalized) {
268
- throw new Error(`Unknown module: ${moduleName}`);
269
- }
270
- return normalized;
271
- });
272
- return {
273
- modules: Array.from(new Set(normalizedModules)),
274
- skipInstall: flags.skipInstall === true || env.ANHEDRAL_SKIP_INSTALL === '1',
275
- dryRun: flags.dryRun === true,
276
- json: flags.json === true,
277
- ...((flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN) != null
278
- ? { toolchainChannel: resolveToolchainChannel(flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN) }
279
- : {}),
280
- };
281
- }
@@ -1,6 +0,0 @@
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>[];
@@ -1,35 +0,0 @@
1
- const ROOT_ENVIRONMENT_ORDER = [
2
- 'ANHEDRAL_DEMO',
3
- 'PORT',
4
- 'TRUST_PROXY_HOPS',
5
- 'CORS_ORIGINS',
6
- 'BASE_URL',
7
- 'DATABASE_URL',
8
- 'CLERK_PUBLISHABLE_KEY',
9
- 'CLERK_SECRET_KEY',
10
- 'RC_WEBHOOK_SECRET',
11
- 'RC_SECRET_API_KEY',
12
- 'RC_ENTITLEMENT_ID',
13
- 'ABLY_API_KEY',
14
- 'R2_ACCOUNT_ID',
15
- 'R2_ACCESS_KEY_ID',
16
- 'R2_SECRET_ACCESS_KEY',
17
- 'R2_BUCKET_NAME',
18
- 'R2_PREFIX',
19
- 'R2_PROXY_READ_URL_TTL_SECONDS',
20
- 'DESKTOP_UPDATE_BASE_URL',
21
- 'CLOUDFLARE_API_TOKEN',
22
- 'CRON_SECRET',
23
- ];
24
- export function composeRootEnvironment(model, context) {
25
- const environment = new Map(model.environment.map((entry) => [entry.name, entry.defaultValue]));
26
- return Object.freeze(ROOT_ENVIRONMENT_ORDER.flatMap((name) => {
27
- if (!environment.has(name))
28
- return [];
29
- const value = name === 'CORS_ORIGINS' ? context.corsOrigins.join(',') : environment.get(name);
30
- return [`${name}=${value}`];
31
- }));
32
- }
33
- export function composeVercelCrons(model) {
34
- return Object.freeze(model.crons.map(({ path, schedule }) => Object.freeze({ path, schedule })));
35
- }
package/dist/print.d.ts DELETED
@@ -1,8 +0,0 @@
1
- export declare const anhedralPrint: {
2
- banner(message: string): void;
3
- section(message: string): void;
4
- step(message: string): void;
5
- done(message: string): void;
6
- info(message: string): void;
7
- fail(message: string): void;
8
- };
package/dist/print.js DELETED
@@ -1,48 +0,0 @@
1
- import { stdout } from 'node:process';
2
- const USE_COLOR = Boolean(stdout.isTTY) && process.env.NO_COLOR == null;
3
- function isQuiet() {
4
- return process.env.ANHEDRAL_QUIET === '1';
5
- }
6
- function paint(code, message) {
7
- if (!USE_COLOR)
8
- return message;
9
- return `\x1b[${code}m${message}\x1b[0m`;
10
- }
11
- const dim = (value) => paint('2', value);
12
- const bold = (value) => paint('1', value);
13
- const cyan = (value) => paint('36', value);
14
- const green = (value) => paint('32', value);
15
- const red = (value) => paint('31', value);
16
- const magenta = (value) => paint('35', value);
17
- export const anhedralPrint = {
18
- banner(message) {
19
- if (isQuiet())
20
- return;
21
- console.log(`${bold(magenta('>'))} ${bold(message)}\n`);
22
- },
23
- section(message) {
24
- if (isQuiet())
25
- return;
26
- console.log(`\n${cyan('==')} ${bold(message)}`);
27
- },
28
- step(message) {
29
- if (isQuiet())
30
- return;
31
- console.log(` ${cyan('>')} ${message}`);
32
- },
33
- done(message) {
34
- if (isQuiet())
35
- return;
36
- console.log(` ${green('ok')} ${message}`);
37
- },
38
- info(message) {
39
- if (isQuiet())
40
- return;
41
- console.log(` ${dim(message)}`);
42
- },
43
- fail(message) {
44
- if (isQuiet())
45
- return;
46
- console.log(` ${red('x')} ${message}`);
47
- },
48
- };
package/dist/prompts.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export declare function shouldPromptForInitModules(args: readonly string[], isTTY: boolean): boolean;
2
- export declare function hasUiSelection(args: readonly string[]): boolean;
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 DELETED
@@ -1,41 +0,0 @@
1
- import { isModuleId } from './architecture/modules.js';
2
- function hasModuleSelection(args) {
3
- return args.some((arg) => arg === '--all' || isModuleId(arg.startsWith('--') ? arg.slice(2) : arg));
4
- }
5
- export function shouldPromptForInitModules(args, isTTY) {
6
- return isTTY && !args.includes('--json') && !hasModuleSelection(args);
7
- }
8
- export function hasUiSelection(args) {
9
- return args.some((arg) => arg === '--ui' || arg.startsWith('--ui='));
10
- }
11
- export function parsePromptModuleSelection(input, fallback, all) {
12
- const tokens = input
13
- .split(/[\s,]+/)
14
- .map((entry) => entry.trim().toLowerCase())
15
- .filter(Boolean);
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)];
31
- }
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 = [];