anhedral 0.8.6 → 0.9.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 (125) hide show
  1. package/README.md +141 -38
  2. package/assets/anhedral-cli-init-technical.svg +15 -15
  3. package/assets/anhedral-cli-init.svg +16 -16
  4. package/dist/access.d.ts +10 -0
  5. package/dist/access.js +93 -0
  6. package/dist/auth-schema-snapshot.d.ts +346 -0
  7. package/dist/auth-schema-snapshot.js +347 -0
  8. package/dist/auth-schema.d.ts +1 -0
  9. package/dist/auth-schema.js +57 -0
  10. package/dist/bin.js +345 -39
  11. package/dist/bounded-file.d.ts +2 -0
  12. package/dist/bounded-file.js +30 -0
  13. package/dist/capabilities.d.ts +24 -42
  14. package/dist/capabilities.js +73 -75
  15. package/dist/cloudflare.d.ts +14 -0
  16. package/dist/cloudflare.js +66 -0
  17. package/dist/command.js +12 -3
  18. package/dist/delivery.d.ts +118 -0
  19. package/dist/delivery.js +611 -0
  20. package/dist/dependencies.d.ts +24 -3
  21. package/dist/dependencies.js +112 -74
  22. package/dist/dependency-order.d.ts +2 -0
  23. package/dist/dependency-order.js +24 -0
  24. package/dist/index.d.ts +18 -3
  25. package/dist/index.js +12 -2
  26. package/dist/managed-hosting.d.ts +49 -0
  27. package/dist/managed-hosting.js +373 -0
  28. package/dist/management.d.ts +3 -0
  29. package/dist/management.js +47 -0
  30. package/dist/platforms/desktop.d.ts +2 -2
  31. package/dist/platforms/desktop.js +25 -15
  32. package/dist/platforms/extension.d.ts +2 -2
  33. package/dist/platforms/extension.js +104 -66
  34. package/dist/platforms/mobile.d.ts +2 -2
  35. package/dist/platforms/mobile.js +13 -7
  36. package/dist/pnpm-policy.d.ts +0 -2
  37. package/dist/pnpm-policy.js +18 -13
  38. package/dist/project-state.d.ts +28 -0
  39. package/dist/project-state.js +226 -0
  40. package/dist/provider-readiness.d.ts +30 -0
  41. package/dist/provider-readiness.js +87 -0
  42. package/dist/provision.d.ts +46 -0
  43. package/dist/provision.js +197 -0
  44. package/dist/readiness.d.ts +4 -2
  45. package/dist/readiness.js +81 -14
  46. package/dist/registry/add.d.ts +22 -0
  47. package/dist/registry/add.js +198 -0
  48. package/dist/registry/bootstrap.d.ts +2 -0
  49. package/dist/registry/bootstrap.js +14 -0
  50. package/dist/registry/commands.d.ts +2 -0
  51. package/dist/registry/commands.js +99 -0
  52. package/dist/registry/context.d.ts +3 -0
  53. package/dist/registry/context.js +21 -0
  54. package/dist/registry/options.d.ts +2 -0
  55. package/dist/registry/options.js +65 -0
  56. package/dist/registry/receipt.d.ts +3 -0
  57. package/dist/registry/receipt.js +73 -0
  58. package/dist/registry/remote.d.ts +25 -0
  59. package/dist/registry/remote.js +168 -0
  60. package/dist/registry/resolve.d.ts +10 -0
  61. package/dist/registry/resolve.js +98 -0
  62. package/dist/registry/types.d.ts +64 -0
  63. package/dist/registry/usage.d.ts +1 -0
  64. package/dist/registry/usage.js +38 -0
  65. package/dist/render.d.ts +0 -4
  66. package/dist/render.js +3 -16
  67. package/dist/repository.d.ts +24 -0
  68. package/dist/repository.js +96 -0
  69. package/dist/scaffold/api.d.ts +2 -0
  70. package/dist/scaffold/api.js +13 -0
  71. package/dist/scaffold/auth.d.ts +3 -0
  72. package/dist/scaffold/auth.js +95 -0
  73. package/dist/scaffold/bootstrap.d.ts +2 -0
  74. package/dist/scaffold/bootstrap.js +34 -0
  75. package/dist/scaffold/compose.d.ts +4 -0
  76. package/dist/scaffold/compose.js +34 -0
  77. package/dist/scaffold/database.d.ts +2 -0
  78. package/dist/scaffold/database.js +43 -0
  79. package/dist/scaffold/documentation.d.ts +3 -0
  80. package/dist/scaffold/documentation.js +91 -0
  81. package/dist/scaffold/environment.d.ts +2 -0
  82. package/dist/scaffold/environment.js +5 -0
  83. package/dist/scaffold/files.d.ts +25 -0
  84. package/dist/scaffold/files.js +108 -0
  85. package/dist/scaffold/integrations.d.ts +3 -0
  86. package/dist/scaffold/integrations.js +7 -0
  87. package/dist/scaffold/services.d.ts +5 -0
  88. package/dist/scaffold/services.js +84 -0
  89. package/dist/scaffold/web.d.ts +2 -0
  90. package/dist/scaffold/web.js +102 -0
  91. package/dist/scaffold/workspace.d.ts +3 -0
  92. package/dist/scaffold/workspace.js +150 -0
  93. package/dist/security-backports.js +7 -2
  94. package/dist/stack-registry.d.ts +80 -0
  95. package/dist/stack-registry.js +424 -0
  96. package/dist/standard.d.ts +6 -19
  97. package/dist/standard.js +46 -653
  98. package/dist/transaction.js +47 -56
  99. package/dist/util.js +12 -2
  100. package/dist/vercel.d.ts +98 -0
  101. package/dist/vercel.js +796 -0
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/dist/vinext.d.ts +1 -0
  105. package/dist/vinext.js +83 -0
  106. package/dist/workspace-graph.d.ts +7 -0
  107. package/dist/workspace-graph.js +73 -0
  108. package/docs/application-stack-standard.md +76 -91
  109. package/docs/delivery-workflow.md +83 -0
  110. package/docs/managed-hosting-architecture.md +87 -0
  111. package/docs/sponsorships.md +25 -0
  112. package/package.json +23 -29
  113. package/dist/evidence-validation.d.ts +0 -2
  114. package/dist/evidence-validation.js +0 -18
  115. package/dist/foundation.d.ts +0 -6
  116. package/dist/foundation.js +0 -49
  117. package/dist/progress-view.d.ts +0 -57
  118. package/dist/progress-view.js +0 -69
  119. package/dist/progress.d.ts +0 -95
  120. package/dist/progress.js +0 -327
  121. package/dist/project.d.ts +0 -5
  122. package/dist/standard-products.d.ts +0 -2
  123. package/dist/standard-products.js +0 -6
  124. /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
  125. /package/dist/{project.js → registry/types.js} +0 -0
@@ -0,0 +1,168 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readBoundedFile } from '../bounded-file.js';
3
+ import path from 'node:path';
4
+ import { dependencyOrder } from '../dependency-order.js';
5
+ import { assertSafeTarget } from './receipt.js';
6
+ const identifier = /^[a-z][a-z0-9-]{0,62}$/;
7
+ const limit = 2 * 1024 * 1024;
8
+ /** Declarative source only: no executable recipe callbacks, hooks or destination templates. */
9
+ export function validateSourceRegistry(value) {
10
+ if (!value || typeof value !== 'object')
11
+ throw new Error('Invalid source registry.');
12
+ const registry = value;
13
+ if (registry.schemaVersion !== 1 ||
14
+ typeof registry.name !== 'string' ||
15
+ !identifier.test(registry.name) ||
16
+ !Array.isArray(registry.items) ||
17
+ !registry.items.length ||
18
+ registry.items.length > 100)
19
+ throw new Error('Unsupported source registry schema.');
20
+ const names = new Set();
21
+ const files = new Set();
22
+ for (const item of registry.items) {
23
+ if (!item ||
24
+ typeof item.name !== 'string' ||
25
+ !identifier.test(item.name) ||
26
+ names.has(item.name) ||
27
+ typeof item.version !== 'string' ||
28
+ !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(item.version) ||
29
+ !Array.isArray(item.registryDependencies) ||
30
+ !item.registryDependencies.every((id) => typeof id === 'string' && identifier.test(id)) ||
31
+ !Array.isArray(item.files) ||
32
+ !item.files.length)
33
+ throw new Error('Invalid or duplicate source registry item.');
34
+ names.add(item.name);
35
+ for (const file of item.files) {
36
+ if (!file || typeof file.target !== 'string' || typeof file.content !== 'string')
37
+ throw new Error('Invalid registry file.');
38
+ assertSafeTarget('/registry-validation', file.target);
39
+ if (!/^(?:apps|packages)\/[a-z][a-z0-9-]*\//.test(file.target) ||
40
+ file.target
41
+ .split('/')
42
+ .some((part) => part.startsWith('.') ||
43
+ ['node_modules', 'dist', 'release', 'anhedral.registry.json'].includes(part)) ||
44
+ files.has(file.target))
45
+ throw new Error(`Unsafe or duplicate source target: ${file.target}.`);
46
+ files.add(file.target);
47
+ if (path.basename(file.target) === 'package.json') {
48
+ const manifest = JSON.parse(file.content);
49
+ if (!manifest ||
50
+ typeof manifest !== 'object' ||
51
+ Array.isArray(manifest) ||
52
+ manifest.private !== true ||
53
+ typeof manifest.name !== 'string' ||
54
+ !/^@workspace\/[a-z][a-z0-9-]*$/.test(manifest.name))
55
+ throw new Error('Remote workspace manifests must be private @workspace packages.');
56
+ if (manifest.scripts &&
57
+ Object.keys(manifest.scripts).some((key) => /^(?:preinstall|install|postinstall|preprepare|prepare|postprepare|prepublish|prepublishOnly|pnpm:devPreinstall)$/.test(key)))
58
+ throw new Error('Remote installation hooks are unsupported.');
59
+ for (const field of [
60
+ 'dependencies',
61
+ 'devDependencies',
62
+ 'optionalDependencies',
63
+ 'peerDependencies',
64
+ ]) {
65
+ if (manifest[field] &&
66
+ (typeof manifest[field] !== 'object' ||
67
+ Array.isArray(manifest[field]) ||
68
+ Object.values(manifest[field]).some((version) => typeof version !== 'string' ||
69
+ !/^(?:workspace:[*^~]|[~^]?\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)$/.test(version))))
70
+ throw new Error('Remote dependencies require versioned npm or workspace references.');
71
+ }
72
+ }
73
+ }
74
+ }
75
+ dependencyOrder(new Map(registry.items.map((item) => [item.name, item.registryDependencies])), 'Remote registry');
76
+ return structuredClone(registry);
77
+ }
78
+ export async function loadSourceRegistry(source, fetcher = fetch) {
79
+ let bytes;
80
+ if (/^https?:/.test(source)) {
81
+ const url = new URL(source);
82
+ if (url.protocol !== 'https:' ||
83
+ url.username ||
84
+ url.password ||
85
+ url.hash ||
86
+ url.search ||
87
+ !/^[a-z0-9.-]+$/i.test(url.hostname) ||
88
+ url.hostname === 'localhost' ||
89
+ !url.hostname.includes('.') ||
90
+ /^\d+(?:\.\d+){3}$/.test(url.hostname))
91
+ throw new Error('Remote registries require a public HTTPS URL without credentials or query parameters.');
92
+ const response = await fetcher(url, { redirect: 'error', signal: AbortSignal.timeout(15_000) });
93
+ if (!response.ok || !response.body)
94
+ throw new Error('Cannot retrieve source registry.');
95
+ const reader = response.body.getReader();
96
+ const chunks = [];
97
+ let size = 0;
98
+ try {
99
+ while (true) {
100
+ const result = await reader.read();
101
+ if (result.done)
102
+ break;
103
+ size += result.value.byteLength;
104
+ if (size > limit)
105
+ throw new Error('Source registry exceeds 2 MiB.');
106
+ chunks.push(result.value);
107
+ }
108
+ }
109
+ finally {
110
+ await reader.cancel();
111
+ }
112
+ bytes = Buffer.concat(chunks);
113
+ source = url.href;
114
+ }
115
+ else {
116
+ source = path.resolve(source);
117
+ bytes = readBoundedFile(source, limit);
118
+ }
119
+ if (bytes.length > limit)
120
+ throw new Error('Source registry exceeds 2 MiB.');
121
+ return {
122
+ source,
123
+ integrity: `sha256-${createHash('sha256').update(bytes).digest('base64')}`,
124
+ registry: validateSourceRegistry(JSON.parse(bytes.toString('utf8'))),
125
+ };
126
+ }
127
+ export function resolveSourceItems(registry, selected) {
128
+ const catalog = new Map(registry.items.map((item) => [item.name, item]));
129
+ const graph = new Map();
130
+ const collect = (name) => {
131
+ if (graph.has(name))
132
+ return;
133
+ const item = catalog.get(name);
134
+ if (!item)
135
+ throw new Error(`Unknown remote registry item: ${name}.`);
136
+ graph.set(name, item.registryDependencies);
137
+ item.registryDependencies.forEach(collect);
138
+ };
139
+ selected.forEach(collect);
140
+ return dependencyOrder(graph, 'Remote registry').map((name) => catalog.get(name));
141
+ }
142
+ /** Compile repository-relative source paths into a distributable JSON payload. */
143
+ export function buildSourceRegistry(manifestFile) {
144
+ const root = path.dirname(path.resolve(manifestFile));
145
+ const registry = JSON.parse(readBoundedFile(manifestFile, limit).toString('utf8'));
146
+ if (!Array.isArray(registry.items))
147
+ throw new Error('Invalid source registry manifest.');
148
+ for (const item of registry.items) {
149
+ if (!Array.isArray(item.files))
150
+ throw new Error('Invalid registry file list.');
151
+ for (const file of item.files) {
152
+ if (file.path === undefined)
153
+ continue;
154
+ if (typeof file.path !== 'string' || file.content !== undefined)
155
+ throw new Error('Declare one source path or embedded content.');
156
+ assertSafeTarget(root, file.path);
157
+ if (!/^(?:apps|packages)\/[a-z][a-z0-9-]*\//.test(file.path) ||
158
+ file.path.split('/').some((part) => part.startsWith('.') || part === 'node_modules'))
159
+ throw new Error('Registry source paths must stay in application or package source.');
160
+ const source = path.join(root, file.path);
161
+ file.content = readBoundedFile(source, limit).toString('utf8');
162
+ delete file.path;
163
+ }
164
+ }
165
+ if (Buffer.byteLength(JSON.stringify(registry)) > limit)
166
+ throw new Error('Source registry exceeds 2 MiB.');
167
+ return validateSourceRegistry(registry);
168
+ }
@@ -0,0 +1,10 @@
1
+ import type { Hosting, StandardOptions, StandardProduct, StackRegistryItem } from './types.js';
2
+ /** Apply default selections once. All callers then use the same strict graph resolver. */
3
+ export declare function resolveStandardProducts(selected: Set<StandardProduct>, hosting: Hosting, hostingSpecified: boolean): void;
4
+ export interface ResolvedStackRegistry {
5
+ schemaVersion: number;
6
+ name: string;
7
+ cliVersion: string;
8
+ items: StackRegistryItem[];
9
+ }
10
+ export declare function resolveStackRegistry(options: Pick<StandardOptions, 'products' | 'hosting'> & Partial<Pick<StandardOptions, 'name'>>): ResolvedStackRegistry;
@@ -0,0 +1,98 @@
1
+ import { dependencyOrder } from '../dependency-order.js';
2
+ import { STANDARD_PRODUCTS, STACK_REGISTRY, getStackDefinition, getStackItem, } from '../stack-registry.js';
3
+ import { createResolutionContext } from './context.js';
4
+ function hasServer(context) {
5
+ return context.has('hono') || context.has('vinext') || context.has('next');
6
+ }
7
+ function needsWorker(name, context) {
8
+ const definition = getStackDefinition(name);
9
+ return (definition.runtime === 'worker' ||
10
+ (definition.runtime === 'optional-worker' && hasServer(context)));
11
+ }
12
+ function validateProducts(selected, hosting) {
13
+ if (!['cloudflare', 'vercel'].includes(hosting))
14
+ throw new Error('hosting must be cloudflare or vercel.');
15
+ for (const id of selected) {
16
+ if (!STANDARD_PRODUCTS.includes(id))
17
+ throw new Error(`Unknown stack product: ${id}.`);
18
+ const { conflicts = [], requiresAny = [], requiresAll = [] } = getStackDefinition(id).selection;
19
+ const conflict = conflicts.find((other) => selected.has(other));
20
+ if (conflict)
21
+ throw new Error(`Choose one of ${id} / ${conflict}.`);
22
+ if (requiresAny.length && !requiresAny.some((other) => selected.has(other)))
23
+ throw new Error(`${id} requires an explicitly selected ${requiresAny.join(' or ')} capability.`);
24
+ const missing = requiresAll.filter((other) => !selected.has(other));
25
+ if (missing.length)
26
+ throw new Error(`${id} requires ${missing.join(', ')}; select it explicitly.`);
27
+ }
28
+ if (selected.has('vinext') && hosting !== 'cloudflare')
29
+ throw new Error('Vinext requires Cloudflare hosting; select --next for Vercel.');
30
+ }
31
+ /** Apply default selections once. All callers then use the same strict graph resolver. */
32
+ export function resolveStandardProducts(selected, hosting, hostingSpecified) {
33
+ for (const id of selected)
34
+ getStackDefinition(id);
35
+ if (![...selected].some((id) => getStackDefinition(id).type === 'application'))
36
+ selected.add(hosting === 'vercel' ? 'next' : 'vinext');
37
+ for (const id of selected) {
38
+ const definition = getStackDefinition(id);
39
+ const alternatives = definition.selection.requiresAny ?? [];
40
+ if (definition.fallbackDependency &&
41
+ !alternatives.some((other) => selected.has(other)))
42
+ selected.add(definition.fallbackDependency);
43
+ }
44
+ const context = createResolutionContext({ products: [...selected], hosting });
45
+ if (context.has('next') &&
46
+ hosting === 'vercel' &&
47
+ [...selected].some((id) => needsWorker(id, context)))
48
+ selected.add('hono');
49
+ if (hostingSpecified && !selected.has('next') && !selected.has('vinext'))
50
+ throw new Error('--hosting applies to the web application.');
51
+ resolveStackRegistry({ products: [...selected], hosting });
52
+ }
53
+ export function resolveStackRegistry(options) {
54
+ const selected = new Set(options.products);
55
+ validateProducts(selected, options.hosting);
56
+ if (![...selected].some((id) => getStackDefinition(id).type === 'application'))
57
+ throw new Error('Stack registry requires an application.');
58
+ const context = createResolutionContext(options);
59
+ const resolved = new Map();
60
+ const collect = (name) => {
61
+ if (resolved.has(name))
62
+ return;
63
+ const definition = getStackDefinition(name);
64
+ const metadata = getStackItem(name);
65
+ const dependencies = new Set(definition.registryDependencies);
66
+ if (definition.runtime) {
67
+ if (!hasServer(context) && definition.runtime !== 'optional-worker')
68
+ throw new Error(`${name} requires a server runtime.`);
69
+ if (needsWorker(name, context) &&
70
+ !context.has('hono') &&
71
+ !(options.hosting === 'cloudflare' && (context.has('vinext') || context.has('next'))))
72
+ throw new Error(`${name} requires a Cloudflare runtime.`);
73
+ if (hasServer(context))
74
+ dependencies.add(context.has('hono') ? 'hono' : context.has('vinext') ? 'vinext' : 'next');
75
+ }
76
+ for (const dependency of definition.resolveDependencies?.(context) ?? [])
77
+ dependencies.add(dependency);
78
+ metadata.registryDependencies = [...dependencies];
79
+ metadata.targets = definition.resolveTargets?.(context) ?? metadata.targets;
80
+ if (definition.configure)
81
+ metadata.bindings = definition.configure(context);
82
+ if (definition.package)
83
+ metadata.package =
84
+ definition.resolvePackage?.(context, definition.package) ?? definition.package;
85
+ resolved.set(name, metadata);
86
+ for (const dependency of dependencies)
87
+ collect(dependency);
88
+ };
89
+ for (const name of ['workspace', 'typescript', 'eslint', ...[...selected].sort()])
90
+ collect(name);
91
+ const graph = new Map([...resolved].map(([name, item]) => [name, item.registryDependencies]));
92
+ return {
93
+ schemaVersion: STACK_REGISTRY.schemaVersion,
94
+ name: STACK_REGISTRY.name,
95
+ cliVersion: STACK_REGISTRY.cliVersion,
96
+ items: dependencyOrder(graph, 'Stack registry').map((name) => resolved.get(name)),
97
+ };
98
+ }
@@ -0,0 +1,64 @@
1
+ import type { StandardProduct } from '../stack-registry.js';
2
+ import type { Json } from '../scaffold/files.js';
3
+ export type { StandardProduct } from '../stack-registry.js';
4
+ export type Hosting = 'cloudflare' | 'vercel';
5
+ export interface StandardOptions {
6
+ root: string;
7
+ name: string;
8
+ products: StandardProduct[];
9
+ hosting: Hosting;
10
+ skipInstall: boolean;
11
+ dryRun: boolean;
12
+ json: boolean;
13
+ initializeGit: boolean;
14
+ }
15
+ export type StandardRunner = (command: string, args: string[], cwd: string) => void;
16
+ export interface StackSelection {
17
+ conflicts?: readonly string[];
18
+ requiresAny?: readonly string[];
19
+ requiresAll?: readonly string[];
20
+ }
21
+ export interface SetupRequirements {
22
+ purpose: string;
23
+ tools: readonly string[];
24
+ access: readonly string[];
25
+ credentials: readonly string[];
26
+ resources: readonly string[];
27
+ verify: readonly string[];
28
+ }
29
+ export interface RecipeContext {
30
+ root: string;
31
+ options: StandardOptions;
32
+ runtimeApp: 'api' | 'web';
33
+ bindings: Json;
34
+ has: (product: string) => boolean;
35
+ }
36
+ export type Recipe = (context: RecipeContext) => void | Promise<void>;
37
+ export interface PackageRecipe {
38
+ name: string;
39
+ source: string;
40
+ dependencies: Record<string, string>;
41
+ }
42
+ export interface StackRegistryItem {
43
+ name: string;
44
+ version: string;
45
+ type: 'foundation' | 'application' | 'service' | 'package' | 'integration';
46
+ description: string;
47
+ registryDependencies: readonly string[];
48
+ targets: readonly string[];
49
+ selection: StackSelection;
50
+ installer: 'bootstrap' | 'recipe' | 'configuration';
51
+ runtime?: 'server' | 'worker' | 'optional-worker';
52
+ environment: readonly string[];
53
+ setup?: SetupRequirements;
54
+ package?: PackageRecipe;
55
+ bindings?: Json;
56
+ }
57
+ export interface ItemDefinition extends Omit<StackRegistryItem, 'bindings'> {
58
+ install?: Recipe;
59
+ configure?: (context: RecipeContext) => Json;
60
+ resolveTargets?: (context: RecipeContext) => string[];
61
+ resolvePackage?: (context: RecipeContext, recipe: PackageRecipe) => PackageRecipe;
62
+ resolveDependencies?: (context: RecipeContext) => string[];
63
+ fallbackDependency?: string;
64
+ }
@@ -0,0 +1 @@
1
+ export declare const STANDARD_USAGE: string;
@@ -0,0 +1,38 @@
1
+ import { STANDARD_PRODUCTS } from '../stack-registry.js';
2
+ export const STANDARD_USAGE = `Anhedral Application Stack & Delivery Standard
3
+
4
+ anhedral new <directory> [products...] [--hosting cloudflare|vercel] [--skip-install] [--no-git] [--dry-run] [--json]
5
+ anhedral add <directory> <items...> [--registry <file|https-url>] [--integrity sha256-...] [--dry-run] [--skip-install] [--json]
6
+ anhedral init [products...] [--hosting cloudflare|vercel] [--skip-install] [--git] [--dry-run] [--json]
7
+
8
+ Default: Vinext on Cloudflare Workers, from the official shadcn pnpm/Turborepo monorepo.
9
+ Web hosting: Vinext on Cloudflare by default; native Next.js on Vercel is an explicit option.
10
+ Products: ${STANDARD_PRODUCTS.join(', ')}
11
+ Examples:
12
+ anhedral new client-app
13
+ anhedral new mobile-app --expo
14
+ anhedral new product --next --expo --hono --neon --clerk --r2
15
+ anhedral new website --next --stripe --resend
16
+ anhedral init --wxt
17
+ anhedral cloud register|stage|save|deploy|status [directory] (managed service API required)
18
+ anhedral registry build <manifest> --output <registry.json>
19
+ anhedral registry list|view [item] [--registry <file|https-url>] [--json]
20
+ anhedral doctor [directory] [--providers] --json
21
+ anhedral repository [directory] [--owner <github-account>] [--name <repo>] [--visibility private|public] [--create]
22
+ anhedral register [directory]
23
+ anhedral connect [directory] [--scope <team>] --audience public|private --use personal|commercial [--create] [--git]
24
+ anhedral connect [directory] --provider cloudflare [--account <id>] --audience public|private [--allow-email <email>]
25
+ anhedral provision [directory] [--dry-run] [--json]
26
+ anhedral ship [directory] [--dry-run] [--json]
27
+ anhedral adopt [directory] --provider vercel [--json]
28
+ anhedral recover [directory] [--provider vercel|cloudflare] [--json]
29
+ anhedral status [directory] [--refresh] [--json]
30
+ anhedral manage [directory] [--local] [--json]
31
+ anhedral verify [directory] [--json]
32
+ anhedral rollback [directory] --release <id> --data-compatible [--json]
33
+
34
+ --skip-install skips the final workspace install; the official shadcn bootstrap
35
+ still requires network access and may install its own dependencies.
36
+ --all is intentionally unsupported: select only required capabilities.
37
+ Vercel Hobby is personal/non-commercial with limits; commercial apps need a suitable existing plan.
38
+ `;
package/dist/render.d.ts CHANGED
@@ -8,7 +8,3 @@ export declare function markdownHeading(value: string): string;
8
8
  export declare function packageNameFromText(value: string): string;
9
9
  export declare function childPackageName(projectName: string, suffix: string): string;
10
10
  export declare function identifierSegment(value: string): string;
11
- /** Project names also become bootstrap directory components; scoped names are not supported. */
12
- export declare function assertProjectName(value: string): string;
13
- /** Provider identifiers obey DNS/R2 limits independently of valid npm names. */
14
- export declare function resourceName(projectName: string, suffix: string): string;
package/dist/render.js CHANGED
@@ -22,9 +22,9 @@ export function markdownHeading(value) {
22
22
  }
23
23
  function assertPackageName(value) {
24
24
  const leafName = value.slice(value.lastIndexOf('/') + 1);
25
- if (value.length > MAX_PACKAGE_NAME_LENGTH
26
- || !PACKAGE_NAME_PATTERN.test(value)
27
- || RESERVED_PACKAGE_NAMES.has(leafName)) {
25
+ if (value.length > MAX_PACKAGE_NAME_LENGTH ||
26
+ !PACKAGE_NAME_PATTERN.test(value) ||
27
+ RESERVED_PACKAGE_NAMES.has(leafName)) {
28
28
  throw new Error(`Invalid package name: ${value}`);
29
29
  }
30
30
  return value;
@@ -72,16 +72,3 @@ export function identifierSegment(value) {
72
72
  .replace(/^-+|-+$/g, '');
73
73
  return segment || 'app';
74
74
  }
75
- /** Project names also become bootstrap directory components; scoped names are not supported. */
76
- export function assertProjectName(value) {
77
- if (typeof value !== 'string' || value.includes('/'))
78
- throw new Error('Project name must be a valid unscoped npm package name.');
79
- return assertPackageName(value);
80
- }
81
- /** Provider identifiers obey DNS/R2 limits independently of valid npm names. */
82
- export function resourceName(projectName, suffix) {
83
- const normalized = identifierSegment(projectName);
84
- const digest = createHash('sha256').update(projectName, 'utf8').digest('hex').slice(0, 8);
85
- const distinct = normalized === projectName ? normalized : `${normalized}-${digest}`;
86
- return `${compactPackageSegment(distinct, 63 - suffix.length - 1)}-${suffix}`;
87
- }
@@ -0,0 +1,24 @@
1
+ import { type DeliveryRunner } from './project-state.js';
2
+ interface Repository {
3
+ repoId: number;
4
+ nameWithOwner: string;
5
+ url: string;
6
+ isPrivate: boolean;
7
+ viewerPermission: string;
8
+ owner: {
9
+ login: string;
10
+ };
11
+ }
12
+ export declare function githubRepository(root: string, run?: DeliveryRunner): Promise<Repository>;
13
+ export declare function setupRepository(root: string, options: {
14
+ owner?: string;
15
+ name?: string;
16
+ create?: boolean;
17
+ visibility: 'private' | 'public';
18
+ }, run?: DeliveryRunner): Promise<{
19
+ repository: Repository;
20
+ branch: string;
21
+ commit: string;
22
+ ownership: string;
23
+ }>;
24
+ export {};
@@ -0,0 +1,96 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { defaultRunner, withApplicationLock } from './project-state.js';
3
+ function environment() {
4
+ if (process.env.GH_HOST && process.env.GH_HOST !== 'github.com')
5
+ throw new Error('Repository automation supports github.com; reconcile the selected GitHub host explicitly.');
6
+ return {
7
+ ...process.env,
8
+ GH_HOST: 'github.com',
9
+ GH_PROMPT_DISABLED: '1',
10
+ GIT_TERMINAL_PROMPT: '0',
11
+ };
12
+ }
13
+ function identity(remote) {
14
+ const match = /^(?:https:\/\/github\.com\/|git@github\.com:)([a-zA-Z0-9-]+\/[a-zA-Z0-9_.-]+?)(?:\.git)?$/.exec(remote);
15
+ if (!match || match[1].endsWith('/.') || match[1].endsWith('/..'))
16
+ throw new Error('Origin must identify one GitHub repository without embedded credentials.');
17
+ return match[1];
18
+ }
19
+ async function ownCheckout(root, run) {
20
+ if (realpathSync((await run('git', ['rev-parse', '--show-toplevel'], root, environment())).trim()) !== root)
21
+ throw new Error('Initialize the app in its own Git repository; do not use a parent repository.');
22
+ }
23
+ export async function githubRepository(root, run = defaultRunner) {
24
+ root = realpathSync(root);
25
+ await ownCheckout(root, run);
26
+ const remotes = (await run('git', ['remote', 'get-url', '--all', 'origin'], root, environment()))
27
+ .trim()
28
+ .split(/\r?\n/);
29
+ const pushes = (await run('git', ['remote', 'get-url', '--push', '--all', 'origin'], root, environment()))
30
+ .trim()
31
+ .split(/\r?\n/);
32
+ if (remotes.length !== 1 || pushes.length !== 1 || identity(remotes[0]) !== identity(pushes[0]))
33
+ throw new Error('Reconcile ambiguous or mismatched GitHub fetch/push origins.');
34
+ const name = identity(remotes[0]);
35
+ const repository = JSON.parse(await run('gh', ['repo', 'view', name, '--json', 'nameWithOwner,url,isPrivate,viewerPermission,owner'], root, environment()));
36
+ if (repository.nameWithOwner?.toLowerCase() !== name.toLowerCase() ||
37
+ repository.url?.toLowerCase() !== `https://github.com/${name}`.toLowerCase() ||
38
+ repository.owner?.login?.toLowerCase() !== name.split('/')[0].toLowerCase() ||
39
+ !['ADMIN', 'MAINTAIN', 'WRITE'].includes(repository.viewerPermission) ||
40
+ typeof repository.isPrivate !== 'boolean')
41
+ throw new Error('GitHub did not verify the selected repository identity and write access.');
42
+ repository.repoId = JSON.parse(await run('gh', ['api', `repos/${name}`, '--jq', '.id'], root, environment()));
43
+ if (!Number.isSafeInteger(repository.repoId) || repository.repoId <= 0)
44
+ throw new Error('GitHub did not return a stable repository ID.');
45
+ return repository;
46
+ }
47
+ export async function setupRepository(root, options, run = defaultRunner) {
48
+ root = realpathSync(root);
49
+ return withApplicationLock(root, async () => {
50
+ await ownCheckout(root, run);
51
+ if ((await run('git', ['status', '--porcelain'], root, environment())).trim())
52
+ throw new Error('Review files and commit the application before repository setup; secrets must remain ignored.');
53
+ const commit = (await run('git', ['rev-parse', 'HEAD'], root, environment())).trim();
54
+ if (!/^[a-f0-9]{40,64}$/.test(commit))
55
+ throw new Error('Create a reviewed initial source commit first.');
56
+ const user = JSON.parse(await run('gh', ['api', 'user'], root, environment()));
57
+ const owner = options.owner ?? user.login;
58
+ const name = options.name ?? root.split('/').at(-1);
59
+ if (!/^[a-zA-Z0-9-]+$/.test(owner ?? '') ||
60
+ !/^[a-zA-Z0-9_.-]+$/.test(name) ||
61
+ ['.', '..'].includes(name))
62
+ throw new Error('Select a valid GitHub account and repository name.');
63
+ const remotes = (await run('git', ['remote'], root, environment())).trim().split(/\r?\n/);
64
+ if (!remotes.includes('origin')) {
65
+ if (!options.create)
66
+ throw new Error('No origin: use --create for an authorized new GitHub repository.');
67
+ await run('gh', [
68
+ 'repo',
69
+ 'create',
70
+ `${owner}/${name}`,
71
+ `--${options.visibility}`,
72
+ '--source',
73
+ '.',
74
+ '--remote',
75
+ 'origin',
76
+ ], root, environment());
77
+ }
78
+ const repository = await githubRepository(root, run);
79
+ if (repository.nameWithOwner.toLowerCase() !== `${owner}/${name}`.toLowerCase() ||
80
+ repository.isPrivate !== (options.visibility === 'private'))
81
+ throw new Error('Existing repository ownership/name/visibility differs; origin and visibility were not replaced.');
82
+ if ((await run('git', ['status', '--porcelain'], root, environment())).trim() ||
83
+ (await run('git', ['rev-parse', 'HEAD'], root, environment())).trim() !== commit)
84
+ throw new Error('Source changed during repository setup; nothing was pushed.');
85
+ const branch = (await run('git', ['symbolic-ref', '--short', 'HEAD'], root, environment())).trim();
86
+ if (!branch || branch.startsWith('-'))
87
+ throw new Error('Select the intended source branch before pushing.');
88
+ await run('git', ['push', '--set-upstream', 'origin', `HEAD:refs/heads/${branch}`], root, environment());
89
+ const remote = (await run('git', ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`], root, environment()))
90
+ .trim()
91
+ .split(/\s+/);
92
+ if (remote[0] !== commit || remote[1] !== `refs/heads/${branch}`)
93
+ throw new Error('GitHub did not confirm the pushed source commit.');
94
+ return { repository, branch, commit, ownership: 'customer' };
95
+ });
96
+ }
@@ -0,0 +1,2 @@
1
+ import type { RecipeContext } from '../registry/types.js';
2
+ export declare function scaffoldApi({ root, options, bindings }: RecipeContext): void;
@@ -0,0 +1,13 @@
1
+ import { STACK_DEPENDENCIES } from '../dependencies.js';
2
+ import { workerConfig, worker } from './files.js';
3
+ export function scaffoldApi({ root, options, bindings }) {
4
+ worker(root, 'api', { ...workerConfig(`${options.name}-api`), ...bindings }, `import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
5
+ const app = new OpenAPIHono<{ Bindings: Env }>();
6
+ app.openapi(createRoute({ method: 'get', path: '/health', responses: { 200: { description: 'Healthy', content: { 'application/json': { schema: z.object({ ok: z.literal(true) }) } } } } }), (c) => c.json({ ok: true as const }, 200));
7
+ app.doc('/openapi.json', { openapi: '3.0.0', info: { title: '${options.name}', version: '1.0.0' } });
8
+ export default app;
9
+ `, {
10
+ hono: STACK_DEPENDENCIES['hono'],
11
+ '@hono/zod-openapi': STACK_DEPENDENCIES['@hono/zod-openapi'],
12
+ });
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { RecipeContext } from '../registry/types.js';
2
+ export declare function scaffoldBetterAuth({ root, options, runtimeApp, has }: RecipeContext): void;
3
+ export declare function scaffoldClerk({ root, has }: RecipeContext): void;