anhedral 0.8.6 → 0.10.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 (123) hide show
  1. package/README.md +137 -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 +349 -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 +16 -3
  25. package/dist/index.js +11 -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 +64 -14
  46. package/dist/registry/add.d.ts +18 -0
  47. package/dist/registry/add.js +163 -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 +48 -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 +2 -0
  57. package/dist/registry/receipt.js +52 -0
  58. package/dist/registry/resolve.d.ts +10 -0
  59. package/dist/registry/resolve.js +98 -0
  60. package/dist/registry/types.d.ts +64 -0
  61. package/dist/registry/usage.d.ts +1 -0
  62. package/dist/registry/usage.js +37 -0
  63. package/dist/render.d.ts +0 -4
  64. package/dist/render.js +3 -16
  65. package/dist/repository.d.ts +24 -0
  66. package/dist/repository.js +96 -0
  67. package/dist/scaffold/api.d.ts +2 -0
  68. package/dist/scaffold/api.js +13 -0
  69. package/dist/scaffold/auth.d.ts +3 -0
  70. package/dist/scaffold/auth.js +95 -0
  71. package/dist/scaffold/bootstrap.d.ts +3 -0
  72. package/dist/scaffold/bootstrap.js +39 -0
  73. package/dist/scaffold/compose.d.ts +4 -0
  74. package/dist/scaffold/compose.js +34 -0
  75. package/dist/scaffold/database.d.ts +2 -0
  76. package/dist/scaffold/database.js +43 -0
  77. package/dist/scaffold/documentation.d.ts +3 -0
  78. package/dist/scaffold/documentation.js +91 -0
  79. package/dist/scaffold/environment.d.ts +2 -0
  80. package/dist/scaffold/environment.js +5 -0
  81. package/dist/scaffold/files.d.ts +25 -0
  82. package/dist/scaffold/files.js +108 -0
  83. package/dist/scaffold/integrations.d.ts +3 -0
  84. package/dist/scaffold/integrations.js +7 -0
  85. package/dist/scaffold/services.d.ts +5 -0
  86. package/dist/scaffold/services.js +84 -0
  87. package/dist/scaffold/web.d.ts +2 -0
  88. package/dist/scaffold/web.js +102 -0
  89. package/dist/scaffold/workspace.d.ts +3 -0
  90. package/dist/scaffold/workspace.js +150 -0
  91. package/dist/security-backports.js +7 -2
  92. package/dist/stack-registry.d.ts +80 -0
  93. package/dist/stack-registry.js +424 -0
  94. package/dist/standard.d.ts +6 -19
  95. package/dist/standard.js +45 -658
  96. package/dist/transaction.js +47 -56
  97. package/dist/util.js +12 -2
  98. package/dist/vercel.d.ts +98 -0
  99. package/dist/vercel.js +796 -0
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/dist/vinext.d.ts +1 -0
  103. package/dist/vinext.js +83 -0
  104. package/dist/workspace-graph.d.ts +7 -0
  105. package/dist/workspace-graph.js +73 -0
  106. package/docs/application-stack-standard.md +76 -91
  107. package/docs/delivery-workflow.md +83 -0
  108. package/docs/managed-hosting-architecture.md +87 -0
  109. package/docs/sponsorships.md +25 -0
  110. package/package.json +23 -29
  111. package/dist/evidence-validation.d.ts +0 -2
  112. package/dist/evidence-validation.js +0 -18
  113. package/dist/foundation.d.ts +0 -6
  114. package/dist/foundation.js +0 -49
  115. package/dist/progress-view.d.ts +0 -57
  116. package/dist/progress-view.js +0 -69
  117. package/dist/progress.d.ts +0 -95
  118. package/dist/progress.js +0 -327
  119. package/dist/project.d.ts +0 -5
  120. package/dist/standard-products.d.ts +0 -2
  121. package/dist/standard-products.js +0 -6
  122. /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
  123. /package/dist/{project.js → registry/types.js} +0 -0
@@ -0,0 +1,163 @@
1
+ import { configureSecurityBackports } from '../security-backports.js';
2
+ import { bootstrapWorkspace } from '../scaffold/bootstrap.js';
3
+ import { registerApplication } from '../project-state.js';
4
+ import { registerVercelApplication } from '../vercel.js';
5
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { isDeepStrictEqual } from 'node:util';
8
+ import { applyEdits, modify, parse } from 'jsonc-parser';
9
+ import { STANDARD_PRODUCTS, installStackItem } from '../stack-registry.js';
10
+ import { inspectProject, validateSetupPlan } from '../readiness.js';
11
+ import { GENERATOR_VERSION } from '../version.js';
12
+ import { runStagedTransaction } from '../transaction.js';
13
+ import { inspectWorkspaceGraph } from '../workspace-graph.js';
14
+ import { execFile } from '../util.js';
15
+ import { json, put, readJson } from '../scaffold/files.js';
16
+ import { writeStandardRoot } from '../scaffold/workspace.js';
17
+ import { createRecipeContext } from './context.js';
18
+ import { resolveStackRegistry, resolveStandardProducts } from './resolve.js';
19
+ import { sourceFiles } from './receipt.js';
20
+ /** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
21
+ export async function addStackItems(root, request, run = execFile) {
22
+ for (const key of Object.keys(request))
23
+ if (!['items', 'dryRun', 'skipInstall'].includes(key))
24
+ throw new Error(`Unknown add option: ${key}. Only bundled registry items are supported.`);
25
+ root = realpathSync(root);
26
+ if (!request.items.length)
27
+ throw new Error('Select at least one registry item.');
28
+ const plan = readJson(root, 'anhedral.setup.json');
29
+ validateSetupPlan(plan);
30
+ if (plan.cliVersion !== GENERATOR_VERSION)
31
+ throw new Error('Use the project CLI version before adding recipes.');
32
+ if (!inspectProject(root).localReady)
33
+ throw new Error('Run doctor and resolve project provenance or workspace errors before add.');
34
+ const receipt = readJson(root, 'anhedral.registry.json');
35
+ if (!receipt.files || typeof receipt.files !== 'object' || Array.isArray(receipt.files))
36
+ throw new Error('This project has no scaffold fingerprints; migrate it deliberately before using add.');
37
+ const baseline = receipt.files;
38
+ const selected = new Set(plan.products);
39
+ for (const id of request.items) {
40
+ if (!STANDARD_PRODUCTS.includes(id))
41
+ throw new Error(`Unknown stack product: ${id}.`);
42
+ selected.add(id);
43
+ }
44
+ resolveStandardProducts(selected, plan.hosting, false);
45
+ const options = {
46
+ root,
47
+ name: plan.name ?? path.basename(root),
48
+ products: [...selected].sort(),
49
+ hosting: plan.hosting,
50
+ skipInstall: !!request.skipInstall,
51
+ dryRun: false,
52
+ json: true,
53
+ initializeGit: false,
54
+ };
55
+ const registry = resolveStackRegistry(options);
56
+ const oldItems = new Map(plan.registry.items.map((item) => [item.name, item]));
57
+ const recipes = registry.items.filter((item) => item.installer === 'recipe' && !isDeepStrictEqual(oldItems.get(item.name), item));
58
+ // Relocating auth, database or service ownership requires a migration, not an additive scaffold.
59
+ for (const item of registry.items) {
60
+ const before = oldItems.get(item.name);
61
+ if (before && before.targets.some((target) => !item.targets.includes(target)))
62
+ throw new Error(`Adding this selection relocates ${item.name}; migrate its existing source explicitly.`);
63
+ }
64
+ const before = sourceFiles(root);
65
+ const receiptText = readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8');
66
+ const commitPaths = [];
67
+ const conflicts = [];
68
+ const result = {
69
+ schemaVersion: 1,
70
+ root,
71
+ dryRun: !!request.dryRun,
72
+ items: registry.items
73
+ .filter((item) => !isDeepStrictEqual(oldItems.get(item.name), item))
74
+ .map((item) => item.name),
75
+ bootstrapRequired: recipes.some((item) => ['vinext', 'next'].includes(item.name)) &&
76
+ !existsSync(path.join(root, 'apps/web/package.json')),
77
+ filesChecked: true,
78
+ targets: recipes.flatMap((item) => item.targets),
79
+ files: commitPaths,
80
+ conflicts,
81
+ };
82
+ // The upstream web generator requires network access and executes package scripts.
83
+ // A preview reports the resolved selection without invoking that generator.
84
+ if (request.dryRun && result.bootstrapRequired) {
85
+ result.filesChecked = false;
86
+ return result;
87
+ }
88
+ await runStagedTransaction(root, {
89
+ dryRun: !!request.dryRun,
90
+ commitPaths,
91
+ seedPaths: [...before.keys(), 'anhedral.registry.json'],
92
+ build: async (stage) => {
93
+ const context = createRecipeContext(stage, options);
94
+ for (const item of registry.items)
95
+ Object.assign(context.bindings, item.bindings);
96
+ if (recipes.some((item) => ['vinext', 'next'].includes(item.name)) &&
97
+ !existsSync(path.join(stage, 'apps/web/package.json')))
98
+ bootstrapWorkspace(stage, options.name, run, true);
99
+ for (const item of recipes)
100
+ await installStackItem(item, context);
101
+ // Configuration items affect the existing runtime without regenerating its application source.
102
+ const configurationItems = registry.items.filter((item) => !isDeepStrictEqual(oldItems.get(item.name), item) && item.bindings);
103
+ if (configurationItems.length) {
104
+ const file = `apps/${context.runtimeApp}/wrangler.jsonc`;
105
+ let content = readFileSync(path.join(stage, file), 'utf8');
106
+ const config = parse(content);
107
+ for (const item of configurationItems)
108
+ for (const [key, value] of Object.entries(item.bindings)) {
109
+ if (config[key] !== undefined && !isDeepStrictEqual(config[key], value))
110
+ throw new Error(`Existing ${key} configuration conflicts with ${item.name}; configure it explicitly.`);
111
+ content = applyEdits(content, modify(content, [key], value, {
112
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
113
+ }));
114
+ }
115
+ put(stage, file, content);
116
+ }
117
+ writeStandardRoot(stage, options, registry, true);
118
+ if (options.products.includes('next') && options.hosting === 'vercel')
119
+ registerVercelApplication(stage);
120
+ if ([...sourceFiles(stage).keys()].some((file) => /^apps\/[^/]+\/wrangler\.jsonc$/.test(file)))
121
+ registerApplication(stage, true);
122
+ if (!request.dryRun)
123
+ run('pnpm', ['install', '--lockfile-only', '--no-frozen-lockfile', '--ignore-scripts'], stage);
124
+ if (!request.dryRun && configureSecurityBackports(stage))
125
+ run('pnpm', ['install', '--lockfile-only', '--no-frozen-lockfile', '--ignore-scripts'], stage);
126
+ const after = sourceFiles(stage);
127
+ const files = { ...baseline };
128
+ for (const [file, hash] of after) {
129
+ if (before.get(file) === hash)
130
+ continue;
131
+ if (before.has(file) &&
132
+ baseline[file] !== before.get(file) &&
133
+ file !== 'anhedral.project.json')
134
+ conflicts.push(file);
135
+ commitPaths.push(file);
136
+ files[file] = hash;
137
+ }
138
+ // Never delete preexisting source as a side effect of an addition.
139
+ for (const file of before.keys())
140
+ if (!after.has(file))
141
+ conflicts.push(file);
142
+ if (conflicts.length && !request.dryRun)
143
+ throw new Error(`Addition conflicts with edited or untracked source: ${conflicts.join(', ')}. No files were installed.`);
144
+ json(stage, 'anhedral.registry.json', {
145
+ ...registry,
146
+ status: 'scaffolded',
147
+ resourcesProvisioned: false,
148
+ workspaces: inspectWorkspaceGraph(stage),
149
+ files,
150
+ });
151
+ commitPaths.push('anhedral.registry.json');
152
+ // Detect concurrent changes even to source that this addition would otherwise preserve.
153
+ if (!isDeepStrictEqual(sourceFiles(root), before) ||
154
+ readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8') !== receiptText)
155
+ throw new Error('Workspace changed during addition; retry after reviewing source.');
156
+ },
157
+ afterCommit: () => {
158
+ if (!request.skipInstall)
159
+ run('pnpm', ['install', '--frozen-lockfile'], root);
160
+ },
161
+ });
162
+ return result;
163
+ }
@@ -0,0 +1,2 @@
1
+ export declare const BOOTSTRAP_COMMAND: readonly ["pnpm", "dlx", "shadcn@4.21.4", "init", "--monorepo", "--template", "next"];
2
+ export declare function bootstrapArguments(name: string, cwd: string): string[];
@@ -0,0 +1,14 @@
1
+ import { SHADCN_VERSION } from '../dependencies.js';
2
+ const SHADCN_PACKAGE = `shadcn@${SHADCN_VERSION}`;
3
+ export const BOOTSTRAP_COMMAND = [
4
+ 'pnpm',
5
+ 'dlx',
6
+ SHADCN_PACKAGE,
7
+ 'init',
8
+ '--monorepo',
9
+ '--template',
10
+ 'next',
11
+ ];
12
+ export function bootstrapArguments(name, cwd) {
13
+ return [...BOOTSTRAP_COMMAND.slice(1), '--defaults', '--name', name, '--cwd', cwd];
14
+ }
@@ -0,0 +1,2 @@
1
+ export declare function runRegistry(args: string[], json: boolean): Promise<void>;
2
+ export declare function runAdd(args: string[]): Promise<void>;
@@ -0,0 +1,48 @@
1
+ import path from 'node:path';
2
+ import { addStackItems } from './add.js';
3
+ import { STACK_REGISTRY, getStackItem } from '../stack-registry.js';
4
+ export async function runRegistry(args, json) {
5
+ const positional = args.filter((arg) => arg !== '--json');
6
+ if (positional.includes('--help') || positional.includes('-h')) {
7
+ console.log('anhedral registry list|view [item] [--json]');
8
+ return;
9
+ }
10
+ if (positional.some((arg) => arg.startsWith('-')))
11
+ throw new Error('Unknown registry option.');
12
+ const [action = 'list', name, ...extra] = positional;
13
+ if (action === 'list' && !name && !extra.length) {
14
+ console.log(json
15
+ ? JSON.stringify(STACK_REGISTRY, null, 2)
16
+ : STACK_REGISTRY.items
17
+ .map((item) => `${item.name} (${item.type}) — ${item.description}`)
18
+ .join('\n'));
19
+ return;
20
+ }
21
+ if (action === 'view' && name && !extra.length) {
22
+ console.log(JSON.stringify(getStackItem(name), null, 2));
23
+ return;
24
+ }
25
+ throw new Error('Use registry list or registry view <item>.');
26
+ }
27
+ export async function runAdd(args) {
28
+ if (args.includes('--help') || args.includes('-h')) {
29
+ console.log('anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]');
30
+ return;
31
+ }
32
+ const positional = [];
33
+ for (const arg of args) {
34
+ if (['--dry-run', '--skip-install', '--json'].includes(arg))
35
+ continue;
36
+ if (arg.startsWith('-'))
37
+ throw new Error(`Unknown add option: ${arg}.`);
38
+ positional.push(arg);
39
+ }
40
+ const [directory, ...items] = positional;
41
+ if (!directory || !items.length)
42
+ throw new Error('Use add <directory> <items...>.');
43
+ console.log(JSON.stringify(await addStackItems(path.resolve(directory), {
44
+ items,
45
+ dryRun: args.includes('--dry-run'),
46
+ skipInstall: args.includes('--skip-install'),
47
+ }), null, 2));
48
+ }
@@ -0,0 +1,3 @@
1
+ import type { RecipeContext, StandardOptions } from './types.js';
2
+ export declare function createRecipeContext(root: string, options: StandardOptions): RecipeContext;
3
+ export declare function createResolutionContext(selection: Pick<StandardOptions, 'products' | 'hosting'> & Partial<Pick<StandardOptions, 'name'>>): RecipeContext;
@@ -0,0 +1,21 @@
1
+ export function createRecipeContext(root, options) {
2
+ const selected = new Set(options.products);
3
+ return {
4
+ root,
5
+ options,
6
+ has: (product) => selected.has(product),
7
+ runtimeApp: selected.has('hono') ? 'api' : 'web',
8
+ bindings: {},
9
+ };
10
+ }
11
+ export function createResolutionContext(selection) {
12
+ return createRecipeContext('', {
13
+ root: '',
14
+ name: selection.name ?? 'app',
15
+ skipInstall: true,
16
+ dryRun: true,
17
+ json: false,
18
+ initializeGit: false,
19
+ ...selection,
20
+ });
21
+ }
@@ -0,0 +1,2 @@
1
+ import type { StandardOptions } from './types.js';
2
+ export declare function parseStandardOptions(command: 'new' | 'init', args: readonly string[]): StandardOptions;
@@ -0,0 +1,65 @@
1
+ import { resolveStandardProducts } from './resolve.js';
2
+ import path from 'node:path';
3
+ import { packageNameFromText } from '../render.js';
4
+ import { STANDARD_PRODUCTS } from '../stack-registry.js';
5
+ function readGitFlag(token, previous) {
6
+ const value = token === '--git';
7
+ if (previous !== undefined && value !== previous)
8
+ throw new Error('Conflicting Git options.');
9
+ return value;
10
+ }
11
+ function readHostingFlag(value, previous, specified) {
12
+ if (value !== 'cloudflare' && value !== 'vercel')
13
+ throw new Error('hosting must be cloudflare or vercel.');
14
+ if (specified && previous !== value)
15
+ throw new Error('Conflicting hosting options.');
16
+ return value;
17
+ }
18
+ function parseStandardFlags(remaining) {
19
+ const products = new Set();
20
+ let hosting = 'cloudflare';
21
+ let hostingSpecified = false;
22
+ let gitSpecified;
23
+ for (let index = 0; index < remaining.length; index++) {
24
+ const token = remaining[index];
25
+ if (['--skip-install', '--dry-run', '--json', '--verbose'].includes(token))
26
+ continue;
27
+ if (token === '--git' || token === '--no-git') {
28
+ gitSpecified = readGitFlag(token, gitSpecified);
29
+ continue;
30
+ }
31
+ if (token === '--hosting' || token.startsWith('--hosting=')) {
32
+ const value = token === '--hosting' ? remaining[++index] : token.slice('--hosting='.length);
33
+ hosting = readHostingFlag(value, hosting, hostingSpecified);
34
+ hostingSpecified = true;
35
+ continue;
36
+ }
37
+ const value = token.replace(/^--/, '');
38
+ if (!STANDARD_PRODUCTS.includes(value)) {
39
+ throw new Error(`Unknown standard product: ${token}. Select only required products.`);
40
+ }
41
+ products.add(value);
42
+ }
43
+ if (!hostingSpecified && products.has('next'))
44
+ hosting = 'vercel';
45
+ return { products, hosting, hostingSpecified, gitSpecified };
46
+ }
47
+ export function parseStandardOptions(command, args) {
48
+ const remaining = [...args];
49
+ const directory = command === 'new' ? remaining.shift() : process.cwd();
50
+ if (!directory || directory.startsWith('-'))
51
+ throw new Error('new requires a destination directory.');
52
+ const root = path.resolve(directory);
53
+ const { products, hosting, hostingSpecified, gitSpecified } = parseStandardFlags(remaining);
54
+ resolveStandardProducts(products, hosting, hostingSpecified);
55
+ return {
56
+ root,
57
+ name: packageNameFromText(path.basename(root)),
58
+ products: [...products].sort(),
59
+ hosting,
60
+ skipInstall: remaining.includes('--skip-install'),
61
+ dryRun: remaining.includes('--dry-run'),
62
+ json: remaining.includes('--json'),
63
+ initializeGit: gitSpecified ?? command === 'new',
64
+ };
65
+ }
@@ -0,0 +1,2 @@
1
+ export declare function sourceFiles(root: string): Map<string, string>;
2
+ export declare function recordScaffoldFiles(root: string): void;
@@ -0,0 +1,52 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { json, readJson } from '../scaffold/files.js';
5
+ const excluded = new Set([
6
+ '.pnpm-store',
7
+ '.artifacts',
8
+ '.git',
9
+ 'node_modules',
10
+ '.next',
11
+ '.open-next',
12
+ '.wrangler',
13
+ '.turbo',
14
+ 'dist',
15
+ '.output',
16
+ '.expo',
17
+ '.fallow',
18
+ '.anhedral',
19
+ '.anhedral.lock',
20
+ '.anhedral-txn',
21
+ 'anhedral.registry.json',
22
+ ]);
23
+ export function sourceFiles(root) {
24
+ const files = new Map();
25
+ const visit = (directory) => {
26
+ for (const entry of readdirSync(path.join(root, directory), { withFileTypes: true })) {
27
+ if (excluded.has(entry.name))
28
+ continue;
29
+ const relative = directory ? `${directory}/${entry.name}` : entry.name;
30
+ // Local credentials are never seeded into an addition transaction or fingerprint receipt.
31
+ if (/^(?:\.env|\.dev\.vars)(?:\.|$)/.test(entry.name) && !entry.name.endsWith('.example'))
32
+ continue;
33
+ if (entry.isSymbolicLink())
34
+ throw new Error(`Source symlinks are unsupported: ${relative}.`);
35
+ if (entry.isDirectory())
36
+ visit(relative);
37
+ else if (entry.isFile())
38
+ files.set(relative, createHash('sha256')
39
+ .update(readFileSync(path.join(root, relative)))
40
+ .digest('hex'));
41
+ }
42
+ };
43
+ visit('');
44
+ return files;
45
+ }
46
+ export function recordScaffoldFiles(root) {
47
+ const receipt = readJson(root, 'anhedral.registry.json');
48
+ json(root, 'anhedral.registry.json', {
49
+ ...receipt,
50
+ files: Object.fromEntries(sourceFiles(root)),
51
+ });
52
+ }
@@ -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,37 @@
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...> [--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 list|view [item] [--json]
19
+ anhedral doctor [directory] [--providers] --json
20
+ anhedral repository [directory] [--owner <github-account>] [--name <repo>] [--visibility private|public] [--create]
21
+ anhedral register [directory]
22
+ anhedral connect [directory] [--scope <team>] --audience public|private --use personal|commercial [--create] [--git]
23
+ anhedral connect [directory] --provider cloudflare [--account <id>] --audience public|private [--allow-email <email>]
24
+ anhedral provision [directory] [--dry-run] [--json]
25
+ anhedral ship [directory] [--dry-run] [--json]
26
+ anhedral adopt [directory] --provider vercel [--json]
27
+ anhedral recover [directory] [--provider vercel|cloudflare] [--json]
28
+ anhedral status [directory] [--refresh] [--json]
29
+ anhedral manage [directory] [--local] [--json]
30
+ anhedral verify [directory] [--json]
31
+ anhedral rollback [directory] --release <id> --data-compatible [--json]
32
+
33
+ --skip-install skips the final workspace install; the official shadcn bootstrap
34
+ still requires network access and may install its own dependencies.
35
+ --all is intentionally unsupported: select only required capabilities.
36
+ Vercel Hobby is personal/non-commercial with limits; commercial apps need a suitable existing plan.
37
+ `;
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
- }