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,198 @@
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, assertSafeTarget } from './receipt.js';
20
+ import { loadSourceRegistry, resolveSourceItems } from './remote.js';
21
+ /** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
22
+ export async function addStackItems(root, request, run = execFile) {
23
+ root = realpathSync(root);
24
+ if (!request.items.length)
25
+ throw new Error('Select at least one registry item.');
26
+ const plan = readJson(root, 'anhedral.setup.json');
27
+ validateSetupPlan(plan);
28
+ if (plan.cliVersion !== GENERATOR_VERSION)
29
+ throw new Error('Use the project CLI version before adding recipes.');
30
+ if (!inspectProject(root).localReady)
31
+ throw new Error('Run doctor and resolve project provenance or workspace errors before add.');
32
+ const receipt = readJson(root, 'anhedral.registry.json');
33
+ if (!receipt.files || typeof receipt.files !== 'object' || Array.isArray(receipt.files))
34
+ throw new Error('This project has no scaffold fingerprints; migrate it deliberately before using add.');
35
+ const baseline = receipt.files;
36
+ const selected = new Set(plan.products);
37
+ let remote;
38
+ if (request.registry)
39
+ remote = await loadSourceRegistry(request.registry);
40
+ else
41
+ for (const id of request.items) {
42
+ if (!STANDARD_PRODUCTS.includes(id))
43
+ throw new Error(`Unknown stack product: ${id}.`);
44
+ selected.add(id);
45
+ }
46
+ if (request.integrity && (!remote || remote.integrity !== request.integrity))
47
+ throw new Error('Source registry integrity differs from the reviewed payload.');
48
+ resolveStandardProducts(selected, plan.hosting, false);
49
+ const options = {
50
+ root,
51
+ name: plan.name ?? path.basename(root),
52
+ products: [...selected].sort(),
53
+ hosting: plan.hosting,
54
+ skipInstall: !!request.skipInstall,
55
+ dryRun: false,
56
+ json: true,
57
+ initializeGit: false,
58
+ };
59
+ const registry = resolveStackRegistry(options);
60
+ const oldItems = new Map(plan.registry.items.map((item) => [item.name, item]));
61
+ const recipes = registry.items.filter((item) => item.installer === 'recipe' && !isDeepStrictEqual(oldItems.get(item.name), item));
62
+ // Relocating auth, database or service ownership requires a migration, not an additive scaffold.
63
+ for (const item of registry.items) {
64
+ const before = oldItems.get(item.name);
65
+ if (before && before.targets.some((target) => !item.targets.includes(target)))
66
+ throw new Error(`Adding this selection relocates ${item.name}; migrate its existing source explicitly.`);
67
+ }
68
+ const remoteItems = remote ? resolveSourceItems(remote.registry, request.items) : [];
69
+ if (remote) {
70
+ const previous = Array.isArray(receipt.remoteItems) ? receipt.remoteItems : [];
71
+ for (const item of remoteItems) {
72
+ const identity = `${remote.source}#${item.name}`;
73
+ const targets = item.files.map((file) => file.target);
74
+ for (const installed of previous) {
75
+ if (installed.identity === identity &&
76
+ installed.targets.some((target) => !targets.includes(target)))
77
+ throw new Error(`Remote update relocates ${item.name}; migrate its source explicitly.`);
78
+ if (installed.identity !== identity &&
79
+ installed.targets.some((target) => targets.includes(target)))
80
+ throw new Error(`Remote target is already owned by ${installed.name}.`);
81
+ }
82
+ }
83
+ }
84
+ const before = sourceFiles(root);
85
+ const receiptText = readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8');
86
+ const commitPaths = [];
87
+ const conflicts = [];
88
+ const result = {
89
+ schemaVersion: 1,
90
+ root,
91
+ dryRun: !!request.dryRun,
92
+ items: registry.items
93
+ .filter((item) => !isDeepStrictEqual(oldItems.get(item.name), item))
94
+ .map((item) => item.name),
95
+ remoteItems: remoteItems.map((item) => item.name),
96
+ ...(remote ? { registry: { source: remote.source, integrity: remote.integrity } } : {}),
97
+ files: commitPaths,
98
+ conflicts,
99
+ };
100
+ await runStagedTransaction(root, {
101
+ dryRun: !!request.dryRun,
102
+ commitPaths,
103
+ seedPaths: [...before.keys(), 'anhedral.registry.json'],
104
+ build: async (stage) => {
105
+ const context = createRecipeContext(stage, options);
106
+ for (const item of registry.items)
107
+ Object.assign(context.bindings, item.bindings);
108
+ if (recipes.some((item) => ['vinext', 'next'].includes(item.name)) &&
109
+ !existsSync(path.join(stage, 'apps/web/package.json')))
110
+ bootstrapWorkspace(stage, options.name, run, true);
111
+ for (const item of recipes)
112
+ await installStackItem(item, context);
113
+ // Configuration items affect the existing runtime without regenerating its application source.
114
+ const configurationItems = registry.items.filter((item) => !isDeepStrictEqual(oldItems.get(item.name), item) && item.bindings);
115
+ if (configurationItems.length) {
116
+ const file = `apps/${context.runtimeApp}/wrangler.jsonc`;
117
+ let content = readFileSync(path.join(stage, file), 'utf8');
118
+ const config = parse(content);
119
+ for (const item of configurationItems)
120
+ for (const [key, value] of Object.entries(item.bindings)) {
121
+ if (config[key] !== undefined && !isDeepStrictEqual(config[key], value))
122
+ throw new Error(`Existing ${key} configuration conflicts with ${item.name}; configure it explicitly.`);
123
+ content = applyEdits(content, modify(content, [key], value, {
124
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
125
+ }));
126
+ }
127
+ put(stage, file, content);
128
+ }
129
+ for (const item of remoteItems)
130
+ for (const file of item.files) {
131
+ assertSafeTarget(stage, file.target);
132
+ put(stage, file.target, file.content);
133
+ }
134
+ writeStandardRoot(stage, options, registry, true);
135
+ if (options.products.includes('next') && options.hosting === 'vercel')
136
+ registerVercelApplication(stage);
137
+ if ([...sourceFiles(stage).keys()].some((file) => /^apps\/[^/]+\/wrangler\.jsonc$/.test(file)))
138
+ registerApplication(stage, true);
139
+ if (!request.dryRun)
140
+ run('pnpm', ['install', '--lockfile-only', '--no-frozen-lockfile', '--ignore-scripts'], stage);
141
+ if (!request.dryRun && configureSecurityBackports(stage))
142
+ run('pnpm', ['install', '--lockfile-only', '--no-frozen-lockfile', '--ignore-scripts'], stage);
143
+ const after = sourceFiles(stage);
144
+ const files = { ...baseline };
145
+ for (const [file, hash] of after) {
146
+ if (before.get(file) === hash)
147
+ continue;
148
+ if (before.has(file) &&
149
+ baseline[file] !== before.get(file) &&
150
+ file !== 'anhedral.project.json')
151
+ conflicts.push(file);
152
+ commitPaths.push(file);
153
+ files[file] = hash;
154
+ }
155
+ // Never delete preexisting source as a side effect of an addition.
156
+ for (const file of before.keys())
157
+ if (!after.has(file))
158
+ conflicts.push(file);
159
+ if (conflicts.length && !request.dryRun)
160
+ throw new Error(`Addition conflicts with edited or untracked source: ${conflicts.join(', ')}. No files were installed.`);
161
+ const remoteReceipts = Array.isArray(receipt.remoteItems) ? [...receipt.remoteItems] : [];
162
+ for (const item of remoteItems) {
163
+ const identity = `${remote.source}#${item.name}`;
164
+ const previous = remoteReceipts.findIndex((entry) => entry.identity === identity);
165
+ const entry = {
166
+ identity,
167
+ name: item.name,
168
+ version: item.version,
169
+ source: remote.source,
170
+ integrity: remote.integrity,
171
+ targets: item.files.map((file) => file.target),
172
+ };
173
+ if (previous >= 0)
174
+ remoteReceipts[previous] = entry;
175
+ else
176
+ remoteReceipts.push(entry);
177
+ }
178
+ json(stage, 'anhedral.registry.json', {
179
+ ...registry,
180
+ status: 'scaffolded',
181
+ resourcesProvisioned: false,
182
+ workspaces: inspectWorkspaceGraph(stage),
183
+ files,
184
+ ...(remoteReceipts.length ? { remoteItems: remoteReceipts } : {}),
185
+ });
186
+ commitPaths.push('anhedral.registry.json');
187
+ // Detect concurrent changes even to source that this addition would otherwise preserve.
188
+ if (!isDeepStrictEqual(sourceFiles(root), before) ||
189
+ readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8') !== receiptText)
190
+ throw new Error('Workspace changed during addition; retry after reviewing source.');
191
+ },
192
+ afterCommit: () => {
193
+ if (!request.skipInstall)
194
+ run('pnpm', ['install', '--frozen-lockfile', '--ignore-scripts'], root);
195
+ },
196
+ });
197
+ return result;
198
+ }
@@ -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,99 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { addStackItems } from './add.js';
4
+ import { buildSourceRegistry, loadSourceRegistry } from './remote.js';
5
+ import { STACK_REGISTRY, getStackItem } from '../stack-registry.js';
6
+ export async function runRegistry(args, json) {
7
+ if (args[0] === 'build') {
8
+ args = args.filter((arg) => arg !== '--json');
9
+ if (args.length !== 4 || args[2] !== '--output' || !args[1] || !args[3])
10
+ throw new Error('Use registry build <manifest> --output <registry.json>.');
11
+ const source = path.resolve(args[1]);
12
+ const output = path.resolve(args[3]);
13
+ if (source === output)
14
+ throw new Error('Registry output must differ from its authored manifest.');
15
+ const registry = buildSourceRegistry(source);
16
+ mkdirSync(path.dirname(output), { recursive: true });
17
+ writeFileSync(output, JSON.stringify(registry, null, 2) + '\n');
18
+ console.log(JSON.stringify({ output, items: registry.items.length }));
19
+ return;
20
+ }
21
+ const registryFlag = args.indexOf('--registry');
22
+ if (registryFlag >= 0) {
23
+ const source = args[registryFlag + 1];
24
+ if (!source || source.startsWith('-'))
25
+ throw new Error('Missing registry source.');
26
+ const positional = args.filter((arg, index) => index !== registryFlag && index !== registryFlag + 1 && arg !== '--json');
27
+ const registry = await loadSourceRegistry(source);
28
+ if (positional.length === 1 && positional[0] === 'list')
29
+ console.log(JSON.stringify(registry, null, 2));
30
+ else if (positional.length === 2 && positional[0] === 'view') {
31
+ const item = registry.registry.items.find((item) => item.name === positional[1]);
32
+ if (!item)
33
+ throw new Error('Unknown remote registry item.');
34
+ console.log(JSON.stringify({ source: registry.source, integrity: registry.integrity, item }, null, 2));
35
+ }
36
+ else
37
+ throw new Error('Use registry list|view [item] --registry <file|https-url>.');
38
+ return;
39
+ }
40
+ const positional = args.filter((arg) => arg !== '--json');
41
+ if (positional.includes('--help') || positional.includes('-h')) {
42
+ console.log('anhedral registry list|view [item] [--json]');
43
+ return;
44
+ }
45
+ if (positional.some((arg) => arg.startsWith('-')))
46
+ throw new Error('Unknown registry option.');
47
+ const [action = 'list', name, ...extra] = positional;
48
+ if (action === 'list' && !name && !extra.length) {
49
+ console.log(json
50
+ ? JSON.stringify(STACK_REGISTRY, null, 2)
51
+ : STACK_REGISTRY.items
52
+ .map((item) => `${item.name} (${item.type}) — ${item.description}`)
53
+ .join('\n'));
54
+ return;
55
+ }
56
+ if (action === 'view' && name && !extra.length) {
57
+ console.log(JSON.stringify(getStackItem(name), null, 2));
58
+ return;
59
+ }
60
+ throw new Error('Use registry list or registry view <item>.');
61
+ }
62
+ export async function runAdd(args) {
63
+ if (args.includes('--help') || args.includes('-h')) {
64
+ console.log('anhedral add <directory> <items...> [--registry <file|https-url>] [--integrity sha256-...] [--dry-run] [--skip-install] [--json]');
65
+ return;
66
+ }
67
+ let source;
68
+ let integrity;
69
+ const positional = [];
70
+ for (let index = 0; index < args.length; index++) {
71
+ const arg = args[index];
72
+ if (['--dry-run', '--skip-install', '--json'].includes(arg))
73
+ continue;
74
+ if (arg === '--integrity') {
75
+ if (integrity || !args[index + 1] || !/^sha256-[A-Za-z0-9+/]{43}=$/.test(args[index + 1]))
76
+ throw new Error('Specify one SHA-256 registry integrity.');
77
+ integrity = args[++index];
78
+ }
79
+ else if (arg === '--registry') {
80
+ if (source || !args[index + 1] || args[index + 1].startsWith('-'))
81
+ throw new Error('Specify one registry source.');
82
+ source = args[++index];
83
+ }
84
+ else if (arg.startsWith('-'))
85
+ throw new Error('Unknown add option.');
86
+ else
87
+ positional.push(arg);
88
+ }
89
+ const [directory, ...items] = positional;
90
+ if (!directory || !items.length)
91
+ throw new Error('Use add <directory> <items...>.');
92
+ console.log(JSON.stringify(await addStackItems(path.resolve(directory), {
93
+ items,
94
+ registry: source,
95
+ integrity,
96
+ dryRun: args.includes('--dry-run'),
97
+ skipInstall: args.includes('--skip-install'),
98
+ }), null, 2));
99
+ }
@@ -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,3 @@
1
+ export declare function sourceFiles(root: string): Map<string, string>;
2
+ export declare function recordScaffoldFiles(root: string): void;
3
+ export declare function assertSafeTarget(root: string, relative: string): void;
@@ -0,0 +1,73 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, 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
+ }
53
+ export function assertSafeTarget(root, relative) {
54
+ if (!relative ||
55
+ path.isAbsolute(relative) ||
56
+ relative.includes('\\') ||
57
+ relative
58
+ .split('/')
59
+ .some((part) => !part ||
60
+ part === '.' ||
61
+ part === '..' ||
62
+ part.includes(':') ||
63
+ /[\x00-\x1f]/.test(part) ||
64
+ /[. ]$/.test(part) ||
65
+ /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(part)))
66
+ throw new Error(`Unsafe registry target: ${relative}.`);
67
+ let current = root;
68
+ for (const part of relative.split('/')) {
69
+ current = path.join(current, part);
70
+ if (existsSync(current) && lstatSync(current).isSymbolicLink())
71
+ throw new Error(`Registry target traverses a symlink: ${relative}.`);
72
+ }
73
+ }
@@ -0,0 +1,25 @@
1
+ export interface SourceItem {
2
+ name: string;
3
+ version: string;
4
+ registryDependencies: string[];
5
+ files: {
6
+ target: string;
7
+ content: string;
8
+ }[];
9
+ }
10
+ export interface SourceRegistry {
11
+ schemaVersion: 1;
12
+ name: string;
13
+ items: SourceItem[];
14
+ }
15
+ export interface RemoteRegistry {
16
+ source: string;
17
+ integrity: string;
18
+ registry: SourceRegistry;
19
+ }
20
+ /** Declarative source only: no executable recipe callbacks, hooks or destination templates. */
21
+ export declare function validateSourceRegistry(value: unknown): SourceRegistry;
22
+ export declare function loadSourceRegistry(source: string, fetcher?: typeof fetch): Promise<RemoteRegistry>;
23
+ export declare function resolveSourceItems(registry: SourceRegistry, selected: readonly string[]): SourceItem[];
24
+ /** Compile repository-relative source paths into a distributable JSON payload. */
25
+ export declare function buildSourceRegistry(manifestFile: string): SourceRegistry;