anhedral 0.9.0 → 0.10.1

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 (53) hide show
  1. package/README.md +6 -10
  2. package/assets/security/audit-deps.mjs +6 -1
  3. package/dist/bin.js +6 -2
  4. package/dist/bounded-file.d.ts +1 -0
  5. package/dist/bounded-file.js +1 -0
  6. package/dist/capabilities.d.ts +4 -4
  7. package/dist/capabilities.js +1 -1
  8. package/dist/dependencies.d.ts +32 -0
  9. package/dist/dependencies.js +47 -1
  10. package/dist/index.d.ts +0 -2
  11. package/dist/index.js +0 -1
  12. package/dist/platforms/desktop.js +4 -5
  13. package/dist/platforms/extension.js +9 -73
  14. package/dist/platforms/mobile.js +6 -47
  15. package/dist/readiness.js +12 -21
  16. package/dist/registry/add.d.ts +5 -9
  17. package/dist/registry/add.js +26 -58
  18. package/dist/registry/commands.js +5 -56
  19. package/dist/registry/receipt.d.ts +0 -1
  20. package/dist/registry/receipt.js +2 -22
  21. package/dist/registry/resolve.js +19 -13
  22. package/dist/registry/types.d.ts +1 -0
  23. package/dist/registry/usage.js +2 -3
  24. package/dist/render.d.ts +2 -0
  25. package/dist/render.js +10 -0
  26. package/dist/scaffold/api.js +20 -3
  27. package/dist/scaffold/auth.js +73 -17
  28. package/dist/scaffold/bootstrap.d.ts +3 -0
  29. package/dist/scaffold/bootstrap.js +15 -1
  30. package/dist/scaffold/database.js +12 -6
  31. package/dist/scaffold/documentation.js +11 -8
  32. package/dist/scaffold/environment.d.ts +2 -0
  33. package/dist/scaffold/environment.js +18 -1
  34. package/dist/scaffold/files.d.ts +1 -0
  35. package/dist/scaffold/files.js +15 -4
  36. package/dist/scaffold/services.js +37 -13
  37. package/dist/scaffold/stack-documentation.d.ts +3 -0
  38. package/dist/scaffold/stack-documentation.js +52 -0
  39. package/dist/scaffold/styling.d.ts +3 -0
  40. package/dist/scaffold/styling.js +84 -0
  41. package/dist/scaffold/web.js +31 -11
  42. package/dist/scaffold/workspace.js +38 -22
  43. package/dist/stack-registry.d.ts +2 -2
  44. package/dist/stack-registry.js +38 -11
  45. package/dist/standard.js +3 -8
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/dist/vinext.js +9 -18
  49. package/dist/workspace-graph.js +3 -2
  50. package/docs/delivery-workflow.md +1 -1
  51. package/package.json +1 -1
  52. package/dist/registry/remote.d.ts +0 -25
  53. package/dist/registry/remote.js +0 -168
@@ -1,22 +1,18 @@
1
1
  import type { StandardRunner } from './types.js';
2
2
  export interface AddRequest {
3
3
  items: string[];
4
- registry?: string;
5
- integrity?: string;
6
4
  dryRun?: boolean;
7
5
  skipInstall?: boolean;
8
6
  }
9
7
  /** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
10
8
  export declare function addStackItems(root: string, request: AddRequest, run?: StandardRunner): Promise<{
11
- files: string[];
12
- conflicts: string[];
13
- registry?: {
14
- source: string;
15
- integrity: string;
16
- } | undefined;
17
9
  schemaVersion: number;
18
10
  root: string;
19
11
  dryRun: boolean;
20
12
  items: string[];
21
- remoteItems: string[];
13
+ bootstrapRequired: boolean;
14
+ filesChecked: boolean;
15
+ targets: string[];
16
+ files: string[];
17
+ conflicts: string[];
22
18
  }>;
@@ -1,5 +1,6 @@
1
+ import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from '../bounded-file.js';
1
2
  import { configureSecurityBackports } from '../security-backports.js';
2
- import { bootstrapWorkspace } from '../scaffold/bootstrap.js';
3
+ import { bootstrapWorkspace, assertWorkspaceRuntime } from '../scaffold/bootstrap.js';
3
4
  import { registerApplication } from '../project-state.js';
4
5
  import { registerVercelApplication } from '../vercel.js';
5
6
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
@@ -16,10 +17,14 @@ import { json, put, readJson } from '../scaffold/files.js';
16
17
  import { writeStandardRoot } from '../scaffold/workspace.js';
17
18
  import { createRecipeContext } from './context.js';
18
19
  import { resolveStackRegistry, resolveStandardProducts } from './resolve.js';
19
- import { sourceFiles, assertSafeTarget } from './receipt.js';
20
- import { loadSourceRegistry, resolveSourceItems } from './remote.js';
20
+ import { sourceFiles } from './receipt.js';
21
21
  /** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
22
22
  export async function addStackItems(root, request, run = execFile) {
23
+ for (const key of Object.keys(request))
24
+ if (!['items', 'dryRun', 'skipInstall'].includes(key))
25
+ throw new Error(`Unknown add option: ${key}. Only bundled registry items are supported.`);
26
+ if (!request.dryRun)
27
+ assertWorkspaceRuntime(run);
23
28
  root = realpathSync(root);
24
29
  if (!request.items.length)
25
30
  throw new Error('Select at least one registry item.');
@@ -34,17 +39,11 @@ export async function addStackItems(root, request, run = execFile) {
34
39
  throw new Error('This project has no scaffold fingerprints; migrate it deliberately before using add.');
35
40
  const baseline = receipt.files;
36
41
  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.');
42
+ for (const id of request.items) {
43
+ if (!STANDARD_PRODUCTS.includes(id))
44
+ throw new Error(`Unknown stack product: ${id}.`);
45
+ selected.add(id);
46
+ }
48
47
  resolveStandardProducts(selected, plan.hosting, false);
49
48
  const options = {
50
49
  root,
@@ -65,24 +64,8 @@ export async function addStackItems(root, request, run = execFile) {
65
64
  if (before && before.targets.some((target) => !item.targets.includes(target)))
66
65
  throw new Error(`Adding this selection relocates ${item.name}; migrate its existing source explicitly.`);
67
66
  }
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
67
  const before = sourceFiles(root);
85
- const receiptText = readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8');
68
+ const receiptText = readBoundedFile(path.join(root, 'anhedral.registry.json'), MAX_PROJECT_JSON_BYTES).toString('utf8');
86
69
  const commitPaths = [];
87
70
  const conflicts = [];
88
71
  const result = {
@@ -92,11 +75,19 @@ export async function addStackItems(root, request, run = execFile) {
92
75
  items: registry.items
93
76
  .filter((item) => !isDeepStrictEqual(oldItems.get(item.name), item))
94
77
  .map((item) => item.name),
95
- remoteItems: remoteItems.map((item) => item.name),
96
- ...(remote ? { registry: { source: remote.source, integrity: remote.integrity } } : {}),
78
+ bootstrapRequired: recipes.some((item) => ['vinext', 'next'].includes(item.name)) &&
79
+ !existsSync(path.join(root, 'apps/web/package.json')),
80
+ filesChecked: true,
81
+ targets: recipes.flatMap((item) => item.targets),
97
82
  files: commitPaths,
98
83
  conflicts,
99
84
  };
85
+ // The upstream web generator requires network access and executes package scripts.
86
+ // A preview reports the resolved selection without invoking that generator.
87
+ if (request.dryRun && result.bootstrapRequired) {
88
+ result.filesChecked = false;
89
+ return result;
90
+ }
100
91
  await runStagedTransaction(root, {
101
92
  dryRun: !!request.dryRun,
102
93
  commitPaths,
@@ -126,11 +117,6 @@ export async function addStackItems(root, request, run = execFile) {
126
117
  }
127
118
  put(stage, file, content);
128
119
  }
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
120
  writeStandardRoot(stage, options, registry, true);
135
121
  if (options.products.includes('next') && options.hosting === 'vercel')
136
122
  registerVercelApplication(stage);
@@ -158,40 +144,22 @@ export async function addStackItems(root, request, run = execFile) {
158
144
  conflicts.push(file);
159
145
  if (conflicts.length && !request.dryRun)
160
146
  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
147
  json(stage, 'anhedral.registry.json', {
179
148
  ...registry,
180
149
  status: 'scaffolded',
181
150
  resourcesProvisioned: false,
182
151
  workspaces: inspectWorkspaceGraph(stage),
183
152
  files,
184
- ...(remoteReceipts.length ? { remoteItems: remoteReceipts } : {}),
185
153
  });
186
154
  commitPaths.push('anhedral.registry.json');
187
155
  // Detect concurrent changes even to source that this addition would otherwise preserve.
188
156
  if (!isDeepStrictEqual(sourceFiles(root), before) ||
189
- readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8') !== receiptText)
157
+ readBoundedFile(path.join(root, 'anhedral.registry.json'), MAX_PROJECT_JSON_BYTES).toString('utf8') !== receiptText)
190
158
  throw new Error('Workspace changed during addition; retry after reviewing source.');
191
159
  },
192
160
  afterCommit: () => {
193
161
  if (!request.skipInstall)
194
- run('pnpm', ['install', '--frozen-lockfile', '--ignore-scripts'], root);
162
+ run('pnpm', ['install', '--frozen-lockfile'], root);
195
163
  },
196
164
  });
197
165
  return result;
@@ -1,42 +1,7 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { addStackItems } from './add.js';
4
- import { buildSourceRegistry, loadSourceRegistry } from './remote.js';
5
3
  import { STACK_REGISTRY, getStackItem } from '../stack-registry.js';
6
4
  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
5
  const positional = args.filter((arg) => arg !== '--json');
41
6
  if (positional.includes('--help') || positional.includes('-h')) {
42
7
  console.log('anhedral registry list|view [item] [--json]');
@@ -61,38 +26,22 @@ export async function runRegistry(args, json) {
61
26
  }
62
27
  export async function runAdd(args) {
63
28
  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]');
29
+ console.log('anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]');
65
30
  return;
66
31
  }
67
- let source;
68
- let integrity;
69
32
  const positional = [];
70
- for (let index = 0; index < args.length; index++) {
71
- const arg = args[index];
33
+ for (const arg of args) {
72
34
  if (['--dry-run', '--skip-install', '--json'].includes(arg))
73
35
  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);
36
+ if (arg.startsWith('-'))
37
+ throw new Error(`Unknown add option: ${arg}.`);
38
+ positional.push(arg);
88
39
  }
89
40
  const [directory, ...items] = positional;
90
41
  if (!directory || !items.length)
91
42
  throw new Error('Use add <directory> <items...>.');
92
43
  console.log(JSON.stringify(await addStackItems(path.resolve(directory), {
93
44
  items,
94
- registry: source,
95
- integrity,
96
45
  dryRun: args.includes('--dry-run'),
97
46
  skipInstall: args.includes('--skip-install'),
98
47
  }), null, 2));
@@ -1,3 +1,2 @@
1
1
  export declare function sourceFiles(root: string): Map<string, string>;
2
2
  export declare function recordScaffoldFiles(root: string): void;
3
- export declare function assertSafeTarget(root: string, relative: string): void;
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { json, readJson } from '../scaffold/files.js';
5
5
  const excluded = new Set([
@@ -16,6 +16,7 @@ const excluded = new Set([
16
16
  '.expo',
17
17
  '.fallow',
18
18
  '.anhedral',
19
+ '.anhedral-audit-report.json',
19
20
  '.anhedral.lock',
20
21
  '.anhedral-txn',
21
22
  'anhedral.registry.json',
@@ -50,24 +51,3 @@ export function recordScaffoldFiles(root) {
50
51
  files: Object.fromEntries(sourceFiles(root)),
51
52
  });
52
53
  }
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
- }
@@ -50,6 +50,23 @@ export function resolveStandardProducts(selected, hosting, hostingSpecified) {
50
50
  throw new Error('--hosting applies to the web application.');
51
51
  resolveStackRegistry({ products: [...selected], hosting });
52
52
  }
53
+ function itemDependencies(name, context) {
54
+ const definition = getStackDefinition(name);
55
+ const dependencies = new Set(definition.registryDependencies);
56
+ if (definition.runtime) {
57
+ if (!hasServer(context) && definition.runtime !== 'optional-worker')
58
+ throw new Error(`${name} requires a server runtime.`);
59
+ if (needsWorker(name, context) &&
60
+ !context.has('hono') &&
61
+ !(context.options.hosting === 'cloudflare' && (context.has('vinext') || context.has('next'))))
62
+ throw new Error(`${name} requires a Cloudflare runtime.`);
63
+ if (hasServer(context))
64
+ dependencies.add(context.has('hono') ? 'hono' : context.has('vinext') ? 'vinext' : 'next');
65
+ }
66
+ for (const dependency of definition.resolveDependencies?.(context) ?? [])
67
+ dependencies.add(dependency);
68
+ return dependencies;
69
+ }
53
70
  export function resolveStackRegistry(options) {
54
71
  const selected = new Set(options.products);
55
72
  validateProducts(selected, options.hosting);
@@ -62,20 +79,9 @@ export function resolveStackRegistry(options) {
62
79
  return;
63
80
  const definition = getStackDefinition(name);
64
81
  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);
82
+ const dependencies = itemDependencies(name, context);
78
83
  metadata.registryDependencies = [...dependencies];
84
+ metadata.environment = definition.resolveEnvironment?.(context) ?? metadata.environment;
79
85
  metadata.targets = definition.resolveTargets?.(context) ?? metadata.targets;
80
86
  if (definition.configure)
81
87
  metadata.bindings = definition.configure(context);
@@ -57,6 +57,7 @@ export interface StackRegistryItem {
57
57
  export interface ItemDefinition extends Omit<StackRegistryItem, 'bindings'> {
58
58
  install?: Recipe;
59
59
  configure?: (context: RecipeContext) => Json;
60
+ resolveEnvironment?: (context: RecipeContext) => string[];
60
61
  resolveTargets?: (context: RecipeContext) => string[];
61
62
  resolvePackage?: (context: RecipeContext, recipe: PackageRecipe) => PackageRecipe;
62
63
  resolveDependencies?: (context: RecipeContext) => string[];
@@ -2,7 +2,7 @@ import { STANDARD_PRODUCTS } from '../stack-registry.js';
2
2
  export const STANDARD_USAGE = `Anhedral Application Stack & Delivery Standard
3
3
 
4
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]
5
+ anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]
6
6
  anhedral init [products...] [--hosting cloudflare|vercel] [--skip-install] [--git] [--dry-run] [--json]
7
7
 
8
8
  Default: Vinext on Cloudflare Workers, from the official shadcn pnpm/Turborepo monorepo.
@@ -15,8 +15,7 @@ Examples:
15
15
  anhedral new website --next --stripe --resend
16
16
  anhedral init --wxt
17
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]
18
+ anhedral registry list|view [item] [--json]
20
19
  anhedral doctor [directory] [--providers] --json
21
20
  anhedral repository [directory] [--owner <github-account>] [--name <repo>] [--visibility private|public] [--create]
22
21
  anhedral register [directory]
package/dist/render.d.ts CHANGED
@@ -8,3 +8,5 @@ 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
+ /** Keep provider resource names within the shared Workers/R2/Queues naming limits. */
12
+ export declare function resourceName(projectName: string, suffix: string): string;
package/dist/render.js CHANGED
@@ -72,3 +72,13 @@ export function identifierSegment(value) {
72
72
  .replace(/^-+|-+$/g, '');
73
73
  return segment || 'app';
74
74
  }
75
+ /** Keep provider resource names within the shared Workers/R2/Queues naming limits. */
76
+ export function resourceName(projectName, suffix) {
77
+ const segment = identifierSegment(projectName);
78
+ const qualifier = identifierSegment(suffix);
79
+ if (qualifier.length > 40)
80
+ throw new Error('Resource suffix is too long.');
81
+ const digest = createHash('sha256').update(projectName).digest('hex').slice(0, 8);
82
+ const base = segment === projectName ? segment : `${segment}-${digest}`;
83
+ return `${compactPackageSegment(base, 62 - qualifier.length)}-${qualifier}`;
84
+ }
@@ -1,10 +1,27 @@
1
+ import { resourceName, jsString } from '../render.js';
1
2
  import { STACK_DEPENDENCIES } from '../dependencies.js';
2
3
  import { workerConfig, worker } from './files.js';
3
4
  export function scaffoldApi({ root, options, bindings }) {
4
- worker(root, 'api', { ...workerConfig(`${options.name}-api`), ...bindings }, `import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
5
+ worker(root, 'api', { ...workerConfig(resourceName(options.name, 'api')), ...bindings }, `import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
6
+
5
7
  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
+ const health = createRoute({
9
+ method: 'get',
10
+ path: '/health',
11
+ responses: {
12
+ 200: {
13
+ description: 'Healthy',
14
+ content: { 'application/json': { schema: z.object({ ok: z.literal(true) }) } },
15
+ },
16
+ },
17
+ });
18
+
19
+ app.openapi(health, (c) => c.json({ ok: true as const }, 200));
20
+ app.doc('/openapi.json', {
21
+ openapi: '3.0.0',
22
+ info: { title: ${jsString(options.name)}, version: '1.0.0' },
23
+ });
24
+
8
25
  export default app;
9
26
  `, {
10
27
  hono: STACK_DEPENDENCIES['hono'],
@@ -1,3 +1,4 @@
1
+ import { writeEnvironmentFile } from './environment.js';
1
2
  import { STACK_DEPENDENCIES } from '../dependencies.js';
2
3
  import { existsSync, readFileSync } from 'node:fs';
3
4
  import path from 'node:path';
@@ -9,15 +10,20 @@ export function scaffoldBetterAuth({ root, options, runtimeApp, has }) {
9
10
  import { drizzleAdapter } from '@better-auth/drizzle-adapter';
10
11
  import type { createDatabase } from '@workspace/db';
11
12
  export function createAuth(db: ReturnType<typeof createDatabase>, secret: string, baseURL: string) {
12
- return betterAuth({ database: drizzleAdapter(db, { provider: '${has('neon') ? 'pg' : 'sqlite'}', transaction: false }), secret, baseURL, emailAndPassword: { enabled: true } });
13
+ return betterAuth({
14
+ database: drizzleAdapter(db, { provider: '${has('neon') ? 'pg' : 'sqlite'}', transaction: false }),
15
+ secret,
16
+ baseURL,
17
+ emailAndPassword: { enabled: true },
18
+ });
13
19
  }
14
20
  `, {
15
- 'better-auth': '1.7.7',
16
- '@better-auth/drizzle-adapter': '1.7.7',
21
+ 'better-auth': STACK_DEPENDENCIES['better-auth'],
22
+ '@better-auth/drizzle-adapter': STACK_DEPENDENCIES['@better-auth/drizzle-adapter'],
17
23
  '@workspace/db': 'workspace:*',
18
24
  });
19
25
  patchPackage(root, `apps/${runtimeApp}/package.json`, {
20
- dependencies: { '@workspace/auth': 'workspace:*' },
26
+ dependencies: { '@workspace/auth': 'workspace:*', '@workspace/db': 'workspace:*' },
21
27
  });
22
28
  const connection = has('neon') ? 'createDatabase(env.DATABASE_URL)' : 'createDatabase(env.DB)';
23
29
  if (has('hono')) {
@@ -27,7 +33,10 @@ export function createAuth(db: ReturnType<typeof createDatabase>, secret: string
27
33
  const config = readJson(root, 'apps/api/wrangler.jsonc');
28
34
  json(root, 'apps/api/wrangler.jsonc', {
29
35
  ...config,
30
- vars: { BETTER_AUTH_URL: 'http://localhost:8787' },
36
+ vars: {
37
+ BETTER_AUTH_URL: 'http://localhost:8787',
38
+ ...(config.vars ?? {}),
39
+ },
31
40
  });
32
41
  }
33
42
  else {
@@ -48,14 +57,21 @@ export { handler as GET, handler as POST };
48
57
  const config = readJson(root, 'apps/web/wrangler.jsonc');
49
58
  json(root, 'apps/web/wrangler.jsonc', {
50
59
  ...config,
51
- vars: { BETTER_AUTH_URL: 'http://localhost:3000' },
60
+ vars: {
61
+ BETTER_AUTH_URL: 'http://localhost:3000',
62
+ ...(config.vars ?? {}),
63
+ },
52
64
  });
53
65
  }
54
66
  patchPackage(root, 'apps/web/package.json', {
55
67
  dependencies: { 'drizzle-orm': STACK_DEPENDENCIES['drizzle-orm'] },
56
68
  });
57
69
  }
58
- put(root, `apps/${runtimeApp}/${runtimeApp === 'web' && options.hosting === 'vercel' ? '.env.example' : '.dev.vars.example'}`, `${has('neon') ? 'DATABASE_URL=\n' : ''}BETTER_AUTH_SECRET=\nBETTER_AUTH_URL=http://localhost:${runtimeApp === 'web' && (options.hosting === 'vercel' || has('vinext')) ? '3000' : '8787'}\n`);
70
+ writeEnvironmentFile(root, `apps/${runtimeApp}/${runtimeApp === 'web' && options.hosting === 'vercel' ? '.env.example' : '.dev.vars.example'}`, {
71
+ ...(has('neon') ? { DATABASE_URL: '' } : {}),
72
+ BETTER_AUTH_SECRET: '',
73
+ BETTER_AUTH_URL: `http://localhost:${runtimeApp === 'web' ? '3000' : '8787'}`,
74
+ });
59
75
  if (has('hono'))
60
76
  put(root, 'apps/api/src/secrets.d.ts', 'interface Env { BETTER_AUTH_SECRET: string }\n');
61
77
  }
@@ -74,22 +90,62 @@ export function scaffoldClerk({ root, has }) {
74
90
  put(root, 'apps/web/app/layout.tsx', 'import { ClerkProvider } from "@clerk/nextjs";\n' + wrapped);
75
91
  }
76
92
  }
77
- put(root, 'apps/web/app/sign-in/[[...sign-in]]/page.tsx', 'import { SignIn } from "@clerk/nextjs";\nexport default function SignInPage() { return <SignIn />; }\n');
78
- put(root, 'apps/web/app/sign-up/[[...sign-up]]/page.tsx', 'import { SignUp } from "@clerk/nextjs";\nexport default function SignUpPage() { return <SignUp />; }\n');
93
+ put(root, 'apps/web/app/sign-in/[[...sign-in]]/page.tsx', `import { Suspense } from 'react';
94
+ import { SignIn } from '@clerk/nextjs';
95
+
96
+ export default function SignInPage() {
97
+ return (
98
+ <Suspense fallback={<p role="status">Loading sign in…</p>}>
99
+ <SignIn />
100
+ </Suspense>
101
+ );
102
+ }
103
+ `);
104
+ put(root, 'apps/web/app/sign-up/[[...sign-up]]/page.tsx', `import { Suspense } from 'react';
105
+ import { SignUp } from '@clerk/nextjs';
106
+
107
+ export default function SignUpPage() {
108
+ return (
109
+ <Suspense fallback={<p role="status">Loading sign up…</p>}>
110
+ <SignUp />
111
+ </Suspense>
112
+ );
113
+ }
114
+ `);
79
115
  put(root, 'apps/web/proxy.ts', `import { clerkMiddleware } from '@clerk/nextjs/server';
80
- export default clerkMiddleware();
116
+ import type { NextMiddleware } from 'next/server';
117
+
118
+ const proxy: NextMiddleware = clerkMiddleware();
119
+ export default proxy;
81
120
  export const config = { matcher: ['/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico)).*)', '/(api|trpc)(.*)'] };
82
121
  `);
83
122
  // Middleware makes auth available; product routes must explicitly require identity and authorization.
84
123
  }
85
124
  if (has('hono')) {
86
- patchPackage(root, 'apps/api/package.json', {
87
- dependencies: { '@clerk/backend': STACK_DEPENDENCIES['@clerk/backend'] },
88
- });
89
- put(root, 'apps/api/src/identity.ts', `import { createClerkClient } from '@clerk/backend';
90
- export async function authenticate(request: Request, secretKey: string, authorizedParties: string[]) {
91
- return createClerkClient({ secretKey }).authenticateRequest(request, { authorizedParties });
125
+ sharedPackage(root, 'auth', `import { createClerkClient } from '@clerk/backend';
126
+
127
+ export async function authenticate(
128
+ request: Request,
129
+ secretKey: string,
130
+ publishableKey: string,
131
+ authorizedParties: string[],
132
+ ) {
133
+ if (!secretKey || !publishableKey || authorizedParties.length === 0) {
134
+ throw new Error('Configure Clerk keys and explicit authorized origins before authenticating.');
135
+ }
136
+ return createClerkClient({ secretKey, publishableKey }).authenticateRequest(request, {
137
+ authorizedParties,
138
+ });
92
139
  }
93
- `);
140
+ `, { '@clerk/backend': STACK_DEPENDENCIES['@clerk/backend'] });
141
+ writeEnvironmentFile(root, 'apps/api/.dev.vars.example', {
142
+ CLERK_SECRET_KEY: '',
143
+ CLERK_PUBLISHABLE_KEY: '',
144
+ });
94
145
  }
146
+ if (has('next') || has('vinext'))
147
+ writeEnvironmentFile(root, 'apps/web/.env.example', {
148
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: '',
149
+ CLERK_SECRET_KEY: '',
150
+ });
95
151
  }
@@ -1,2 +1,5 @@
1
1
  import type { StandardRunner } from '../registry/types.js';
2
+ export declare function assertBootstrapPlatform(run: StandardRunner, platform?: NodeJS.Platform): void;
3
+ /** Planning and diagnostics support older Node releases; generated dependencies do not. */
4
+ export declare function assertWorkspaceRuntime(run: StandardRunner, version?: string): void;
2
5
  export declare function bootstrapWorkspace(root: string, name: string, run: StandardRunner, webOnly?: boolean): void;
@@ -1,8 +1,22 @@
1
1
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { appendGitignore } from '../util.js';
3
+ import { execFile, appendGitignore } from '../util.js';
4
4
  import { bootstrapArguments } from '../registry/bootstrap.js';
5
+ export function assertBootstrapPlatform(run, platform = process.platform) {
6
+ if (platform === 'win32' && run === execFile)
7
+ throw new Error('Native Windows initialization is blocked because the upstream shadcn monorepo bootstrap writes component files outside the project. Run Anhedral inside WSL. Planning and doctor remain available on Windows.');
8
+ }
9
+ /** Planning and diagnostics support older Node releases; generated dependencies do not. */
10
+ export function assertWorkspaceRuntime(run, version = process.versions.node) {
11
+ if (run !== execFile)
12
+ return;
13
+ const [major, minor] = version.split('.').map(Number);
14
+ if (!((major === 22 && minor >= 13) || major >= 24))
15
+ throw new Error('Generated workspaces require Node.js 22.13+ on the 22.x line or Node.js 24+. Planning and doctor remain available on older CLI-supported versions.');
16
+ }
5
17
  export function bootstrapWorkspace(root, name, run, webOnly = false) {
18
+ assertBootstrapPlatform(run);
19
+ assertWorkspaceRuntime(run);
6
20
  const temporary = path.join(root, '.bootstrap');
7
21
  mkdirSync(temporary);
8
22
  try {