anhedral 0.10.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 (43) hide show
  1. package/README.md +2 -2
  2. package/assets/security/audit-deps.mjs +6 -1
  3. package/dist/bounded-file.d.ts +1 -0
  4. package/dist/bounded-file.js +1 -0
  5. package/dist/capabilities.d.ts +4 -4
  6. package/dist/capabilities.js +1 -1
  7. package/dist/dependencies.d.ts +32 -0
  8. package/dist/dependencies.js +47 -1
  9. package/dist/platforms/desktop.js +4 -5
  10. package/dist/platforms/extension.js +9 -73
  11. package/dist/platforms/mobile.js +6 -47
  12. package/dist/readiness.js +11 -3
  13. package/dist/registry/add.js +6 -3
  14. package/dist/registry/receipt.js +1 -0
  15. package/dist/registry/resolve.js +19 -13
  16. package/dist/registry/types.d.ts +1 -0
  17. package/dist/render.d.ts +2 -0
  18. package/dist/render.js +10 -0
  19. package/dist/scaffold/api.js +20 -3
  20. package/dist/scaffold/auth.js +73 -17
  21. package/dist/scaffold/bootstrap.d.ts +2 -0
  22. package/dist/scaffold/bootstrap.js +9 -0
  23. package/dist/scaffold/database.js +12 -6
  24. package/dist/scaffold/documentation.js +11 -8
  25. package/dist/scaffold/environment.d.ts +2 -0
  26. package/dist/scaffold/environment.js +18 -1
  27. package/dist/scaffold/files.d.ts +1 -0
  28. package/dist/scaffold/files.js +15 -4
  29. package/dist/scaffold/services.js +37 -13
  30. package/dist/scaffold/stack-documentation.d.ts +3 -0
  31. package/dist/scaffold/stack-documentation.js +52 -0
  32. package/dist/scaffold/styling.d.ts +3 -0
  33. package/dist/scaffold/styling.js +84 -0
  34. package/dist/scaffold/web.js +31 -11
  35. package/dist/scaffold/workspace.js +38 -22
  36. package/dist/stack-registry.d.ts +2 -2
  37. package/dist/stack-registry.js +38 -11
  38. package/dist/standard.js +2 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/dist/vinext.js +9 -18
  42. package/dist/workspace-graph.js +3 -2
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -25,7 +25,7 @@ Starter prompt:
25
25
 
26
26
  Codex asks about the app, explains the stack, implements the product, configures selected services, and delivers into your authorized accounts. The CLI commands below are also available independently.
27
27
 
28
- Use Node.js 22.13+ and pnpm 10.34.5 for generated workspaces. Generate on Linux, macOS, or WSL.
28
+ Use Node.js 22.13+ on the 22.x line or Node.js 24+, with pnpm 10.34.5, for generated workspaces. Generate on Linux, macOS, or WSL.
29
29
 
30
30
  ```sh
31
31
  # Inspect a plan first: no project files or cloud resources are created
@@ -51,7 +51,7 @@ Anhedral Init is free and open source. Sponsorship helps fund ongoing developmen
51
51
 
52
52
  ## Choose your stack
53
53
 
54
- The bundled stack registry drives composition of every supported stack part. Run `anhedral registry list` or `anhedral registry view better-auth --json` to inspect items, destinations, dependencies and selection constraints. `new --dry-run --json` includes the resolved dependency graph. Scaffolding installs recipes in dependency order into `apps/*` and `packages/*`, validates local workspace references and cycles, and writes `anhedral.registry.json` with recipe versions and the resulting workspace graph. The same catalog is bundled with the plugin at build time.
54
+ The bundled stack registry drives composition of every supported stack part. Run `anhedral registry list` or `anhedral registry view better-auth --json` to inspect items, destinations, dependencies and selection constraints. `new --dry-run --json` includes the resolved dependency graph. Scaffolding installs recipes in dependency order into `apps/*` and `packages/*`, validates local workspace references and cycles, and writes `anhedral.registry.json` with recipe versions and the resulting workspace graph. The same catalog is bundled with the plugin at build time. Generated `docs/stack.md` lists the current resolved options, targets, environment names, required setup and verification; additions refresh it without rewriting the application README.
55
55
 
56
56
  Registry items are trusted recipes shipped in the CLI release: foundations, apps, services, shared packages and runtime integrations. They follow shadcn's source ownership and dependency composition practices; this catalog is an Anhedral schema, not a shadcn registry payload. Runtime-aware recipes coordinate bindings, manifests and auth routes across workspaces. Each registry entry owns its recipe, provider requirements, environment variables, bindings and composition rules. Resolved plans name the exact generated destinations, including runtime-dependent packages. Recipe versions follow the CLI release. The official shadcn bootstrap is pinned to 4.21.4; commit the generated source and lockfile to preserve the application artifact. Incremental `add` supports only the bundled catalog maintained and tested by Anhedral. External registries and customer-authored registry payloads are unsupported. Additions refuse to overwrite edited or untracked source; runtime relocation requires a deliberate migration.
57
57
 
@@ -115,11 +115,16 @@ function validateAdvisory(advisory) {
115
115
  ensure(Array.isArray(advisory.findings), 'Malformed pnpm advisory');
116
116
  ensure(advisory.findings.length > 0, 'Malformed pnpm advisory');
117
117
  advisory.findings.forEach(validateFinding);
118
+ const versions = advisory.findings.map((finding) => finding.version);
119
+ ensure(new Set(versions).size === versions.length, 'Duplicate pnpm advisory versions');
118
120
  }
119
121
 
120
122
  function validateCount(advisories, severity, count) {
121
123
  ensure(Number.isInteger(count) && count >= 0, 'Malformed pnpm vulnerability counts');
122
- const details = advisories.filter((advisory) => advisory.severity === severity).length;
124
+ // The pnpm 10 registry endpoint counts affected versions, not unique advisory IDs.
125
+ const details = advisories
126
+ .filter((advisory) => advisory.severity === severity)
127
+ .reduce((total, advisory) => total + advisory.findings.length, 0);
123
128
  ensure(count === details, `Missing ${severity} advisory details: count ${count}, details ${details}`);
124
129
  }
125
130
 
@@ -1,2 +1,3 @@
1
+ export declare const MAX_PROJECT_JSON_BYTES: number;
1
2
  /** Reject devices, links and oversized or growing files before parsing local input. */
2
3
  export declare function readBoundedFile(filePath: string, maximumBytes: number): Buffer;
@@ -1,4 +1,5 @@
1
1
  import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from 'node:fs';
2
+ export const MAX_PROJECT_JSON_BYTES = 2 * 1024 * 1024;
2
3
  /** Reject devices, links and oversized or growing files before parsing local input. */
3
4
  export function readBoundedFile(filePath, maximumBytes) {
4
5
  const stat = lstatSync(filePath);
@@ -6,7 +6,7 @@ export declare const CAPABILITIES: Record<StandardProduct, Capability>;
6
6
  export declare const CAPABILITY_REGISTRY: {
7
7
  schemaVersion: number;
8
8
  cliVersion: string;
9
- capabilities: Record<"realtime" | "electron" | "wxt" | "expo" | "next" | "vinext" | "hono" | "neon" | "better-auth" | "queues" | "workflows" | "d1" | "local-data" | "clerk" | "r2" | "kv" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog" | "styling", Capability>;
9
+ capabilities: Record<"realtime" | "electron" | "wxt" | "expo" | "vinext" | "next" | "workflows" | "styling" | "hono" | "neon" | "d1" | "local-data" | "clerk" | "better-auth" | "r2" | "kv" | "queues" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog", Capability>;
10
10
  };
11
11
  export declare function createSetupPlan(options: StandardOptions, registry?: import("./registry/resolve.js").ResolvedStackRegistry): {
12
12
  schemaVersion: number;
@@ -15,10 +15,10 @@ export declare function createSetupPlan(options: StandardOptions, registry?: imp
15
15
  standard: string;
16
16
  root: string;
17
17
  name: string;
18
- products: ("realtime" | "electron" | "wxt" | "expo" | "next" | "vinext" | "hono" | "neon" | "better-auth" | "queues" | "workflows" | "d1" | "local-data" | "clerk" | "r2" | "kv" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog" | "styling")[];
18
+ products: ("realtime" | "electron" | "wxt" | "expo" | "vinext" | "next" | "workflows" | "styling" | "hono" | "neon" | "d1" | "local-data" | "clerk" | "better-auth" | "r2" | "kv" | "queues" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog")[];
19
19
  hosting: import("./registry/types.js").Hosting;
20
20
  registry: import("./registry/resolve.js").ResolvedStackRegistry;
21
- bootstrap: ("pnpm" | "shadcn@4.21.4" | "dlx" | "init" | "--monorepo" | "--template" | "next")[];
21
+ bootstrap: ("pnpm" | "next" | "shadcn@4.21.4" | "dlx" | "init" | "--monorepo" | "--template")[];
22
22
  dryRun: boolean;
23
23
  setup: {
24
24
  tools: string[];
@@ -32,7 +32,7 @@ export declare function createSetupPlan(options: StandardOptions, registry?: imp
32
32
  credentials: readonly string[];
33
33
  resources: readonly string[];
34
34
  verify: readonly string[];
35
- id: "realtime" | "electron" | "wxt" | "expo" | "next" | "vinext" | "hono" | "neon" | "better-auth" | "queues" | "workflows" | "d1" | "local-data" | "clerk" | "r2" | "kv" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog" | "styling";
35
+ id: "realtime" | "electron" | "wxt" | "expo" | "vinext" | "next" | "workflows" | "styling" | "hono" | "neon" | "d1" | "local-data" | "clerk" | "better-auth" | "r2" | "kv" | "queues" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog";
36
36
  }[];
37
37
  completion: string[];
38
38
  resourcesProvisioned: boolean;
@@ -61,7 +61,7 @@ export function createSetupPlan(options, registry = resolveStackRegistry(options
61
61
  tools: [
62
62
  ...new Set([
63
63
  'Git',
64
- 'Node.js 22.13+ for generated workspaces',
64
+ 'Node.js 22.13+ on 22.x, or Node.js 24+ for generated workspaces',
65
65
  PACKAGE_MANAGER.replace('@', ' '),
66
66
  'GitHub integration or gh',
67
67
  'Fallow',
@@ -2,14 +2,21 @@ export interface DependencyGroup {
2
2
  dependencies?: Record<string, string>;
3
3
  devDependencies?: Record<string, string>;
4
4
  }
5
+ export declare const NATIVE_ESLINT_VERSION = "10.0.3";
5
6
  export declare const DESKTOP_DEPENDENCIES: DependencyGroup;
6
7
  export declare const EXTENSION_DEPENDENCIES: DependencyGroup;
7
8
  export declare const MOBILE_APP_DEPENDENCIES: DependencyGroup;
8
9
  export declare const MOBILE_NATIVEWIND_DEPENDENCIES: DependencyGroup;
10
+ export declare const WORKSPACE_NODE_ENGINE = "^22.13.0 || >=24.0.0";
9
11
  export declare const PACKAGE_MANAGER = "pnpm@10.34.5";
10
12
  export declare const EAS_CLI_VERSION = "21.0.1";
11
13
  export declare const SHADCN_VERSION = "4.21.4";
14
+ export declare const NEXT_VERCEL_VERSION = "16.4.0";
15
+ export declare const NEXT_OPENNEXT_VERSION = "16.3.8";
12
16
  export declare const STACK_DEPENDENCIES: {
17
+ readonly 'better-auth': "1.7.7";
18
+ readonly '@better-auth/drizzle-adapter': "1.7.7";
19
+ readonly '@neondatabase/serverless': "1.2.0";
13
20
  readonly 'drizzle-orm': "0.45.4";
14
21
  readonly '@clerk/nextjs': "7.9.14";
15
22
  readonly '@clerk/backend': "3.23.2";
@@ -30,3 +37,28 @@ export declare const STACK_DEPENDENCIES: {
30
37
  readonly stripe: "20.4.1";
31
38
  readonly 'posthog-node': "5.55.2";
32
39
  };
40
+ /** Vinext's RSC/React versions form a separately verified compatibility group. */
41
+ export declare const VINEXT_DEPENDENCIES: DependencyGroup;
42
+ /** Separate HTTP and inspector ports let all selected Workers run together. */
43
+ export declare const WORKER_DEV_PORTS: {
44
+ readonly api: {
45
+ readonly http: 8787;
46
+ readonly inspector: 9231;
47
+ };
48
+ readonly realtime: {
49
+ readonly http: 8788;
50
+ readonly inspector: 9232;
51
+ };
52
+ readonly jobs: {
53
+ readonly http: 8789;
54
+ readonly inspector: 9233;
55
+ };
56
+ readonly scheduled: {
57
+ readonly http: 8790;
58
+ readonly inspector: 9234;
59
+ };
60
+ readonly workflows: {
61
+ readonly http: 8791;
62
+ readonly inspector: 9235;
63
+ };
64
+ };
@@ -1,3 +1,5 @@
1
+ // renovate: datasource=npm depName=eslint
2
+ export const NATIVE_ESLINT_VERSION = '10.0.3';
1
3
  export const DESKTOP_DEPENDENCIES = {
2
4
  dependencies: {
3
5
  // renovate: datasource=npm depName=react
@@ -24,7 +26,7 @@ export const DESKTOP_DEPENDENCIES = {
24
26
  electron: '43.7.8',
25
27
  // renovate: datasource=npm depName=electron-builder
26
28
  'electron-builder': '26.15.3',
27
- eslint: '10.0.3',
29
+ eslint: NATIVE_ESLINT_VERSION,
28
30
  // renovate: datasource=npm depName=tailwindcss
29
31
  tailwindcss: '4.1.18',
30
32
  // renovate: datasource=npm depName=postcss
@@ -129,12 +131,23 @@ export const MOBILE_NATIVEWIND_DEPENDENCIES = {
129
131
  tailwindcss: '3.4.19',
130
132
  },
131
133
  };
134
+ export const WORKSPACE_NODE_ENGINE = '^22.13.0 || >=24.0.0';
132
135
  export const PACKAGE_MANAGER = 'pnpm@10.34.5';
133
136
  // renovate: datasource=npm depName=eas-cli
134
137
  export const EAS_CLI_VERSION = '21.0.1';
135
138
  // renovate: datasource=npm depName=shadcn
136
139
  export const SHADCN_VERSION = '4.21.4';
140
+ // renovate: datasource=npm depName=next
141
+ export const NEXT_VERCEL_VERSION = '16.4.0';
142
+ // renovate: datasource=npm depName=next
143
+ export const NEXT_OPENNEXT_VERSION = '16.3.8';
137
144
  export const STACK_DEPENDENCIES = {
145
+ // renovate: datasource=npm depName=better-auth
146
+ 'better-auth': '1.7.7',
147
+ // renovate: datasource=npm depName=@better-auth/drizzle-adapter
148
+ '@better-auth/drizzle-adapter': '1.7.7',
149
+ // renovate: datasource=npm depName=@neondatabase/serverless
150
+ '@neondatabase/serverless': '1.2.0',
138
151
  // renovate: datasource=npm depName=drizzle-orm
139
152
  'drizzle-orm': '0.45.4',
140
153
  // renovate: datasource=npm depName=@clerk/nextjs
@@ -174,3 +187,36 @@ export const STACK_DEPENDENCIES = {
174
187
  // renovate: datasource=npm depName=posthog-node
175
188
  'posthog-node': '5.55.2',
176
189
  };
190
+ /** Vinext's RSC/React versions form a separately verified compatibility group. */
191
+ export const VINEXT_DEPENDENCIES = {
192
+ dependencies: {
193
+ // renovate: datasource=npm depName=vinext
194
+ vinext: '1.1.0',
195
+ next: NEXT_VERCEL_VERSION,
196
+ // renovate: datasource=npm depName=react
197
+ react: '19.2.8',
198
+ // renovate: datasource=npm depName=react-dom
199
+ 'react-dom': '19.2.8',
200
+ // renovate: datasource=npm depName=react-server-dom-webpack
201
+ 'react-server-dom-webpack': '19.2.8',
202
+ },
203
+ devDependencies: {
204
+ // renovate: datasource=npm depName=vite
205
+ vite: '8.3.4',
206
+ // renovate: datasource=npm depName=@cloudflare/vite-plugin
207
+ '@cloudflare/vite-plugin': '1.63.1',
208
+ // renovate: datasource=npm depName=@vitejs/plugin-rsc
209
+ '@vitejs/plugin-rsc': '0.5.36',
210
+ // renovate: datasource=npm depName=@vitejs/plugin-react
211
+ '@vitejs/plugin-react': '6.1.2',
212
+ wrangler: STACK_DEPENDENCIES.wrangler,
213
+ },
214
+ };
215
+ /** Separate HTTP and inspector ports let all selected Workers run together. */
216
+ export const WORKER_DEV_PORTS = {
217
+ api: { http: 8787, inspector: 9231 },
218
+ realtime: { http: 8788, inspector: 9232 },
219
+ jobs: { http: 8789, inspector: 9233 },
220
+ scheduled: { http: 8790, inspector: 9234 },
221
+ workflows: { http: 8791, inspector: 9235 },
222
+ };
@@ -107,9 +107,6 @@ export default defineConfig({
107
107
  build: {
108
108
  outDir: 'dist/renderer',
109
109
  emptyOutDir: true,
110
- // Clerk is already loaded behind a dynamic import. Its optional account-management
111
- // surface is intentionally a large async chunk and does not inflate initial rendering.
112
- chunkSizeWarningLimit: 1600,
113
110
  },
114
111
  });
115
112
  `);
@@ -261,7 +258,7 @@ function isAllowedAppNavigation(value: string): boolean {
261
258
  }
262
259
 
263
260
  // Product-owned window options and navigation policy belong in this file.
264
- // Anhedral's managed main.ts imports this seam and wires lifecycle integrations around it.
261
+ // main.ts imports this seam and manages application lifecycle.
265
262
  export function createAppWindow(): BrowserWindow {
266
263
  const window = new BrowserWindow({
267
264
  width: 1180,
@@ -306,11 +303,13 @@ export function createAppWindow(): BrowserWindow {
306
303
  writeFile(path.join(dir, 'src/main/main.ts'), `import { app, BrowserWindow, session } from 'electron';
307
304
  import { createAppWindow } from './app-window.js';
308
305
 
309
-
310
306
  app.whenReady().then(() => {
311
307
  session.defaultSession.setPermissionCheckHandler(() => false);
312
308
  session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
313
309
  createAppWindow();
310
+ }).catch((error) => {
311
+ console.error('Unable to start the desktop app:', error);
312
+ app.quit();
314
313
  });
315
314
  app.on('window-all-closed', () => {
316
315
  if (process.platform !== 'darwin') app.quit();
@@ -1,3 +1,4 @@
1
+ import { NATIVE_ESLINT_VERSION } from '../dependencies.js';
1
2
  import path from 'node:path';
2
3
  import { writeReactLintConfig } from './lint.js';
3
4
  import { writeFile } from '../util.js';
@@ -41,11 +42,14 @@ function writePackageJson(dir, projectName) {
41
42
  typecheck: 'tsc --noEmit',
42
43
  lint: 'eslint src --max-warnings 0',
43
44
  },
44
- dependencies: EXTENSION_DEPENDENCIES.dependencies,
45
+ dependencies: {
46
+ ...EXTENSION_DEPENDENCIES.dependencies,
47
+ '@workspace/styling': 'workspace:*',
48
+ },
45
49
  devDependencies: {
46
50
  ...EXTENSION_DEPENDENCIES.devDependencies,
47
51
  '@workspace/eslint-config': 'workspace:*',
48
- eslint: '10.0.3',
52
+ eslint: NATIVE_ESLINT_VERSION,
49
53
  },
50
54
  }, null, 2) + '\n');
51
55
  }
@@ -55,13 +59,12 @@ function writeWxtConfig(dir, displayName) {
55
59
  const actionTitleLiteral = jsString(`Open ${displayName}`);
56
60
  writeFile(path.join(dir, 'wxt.config.ts'), `import { defineConfig } from 'wxt';
57
61
 
58
-
59
62
  export default defineConfig({
60
63
  srcDir: 'src',
64
+ dev: { server: { port: 3001, strictPort: true } },
61
65
  manifest: () => {
62
66
  const crxPublicKey = import.meta.env.VITE_CRX_PUBLIC_KEY || '';
63
67
 
64
-
65
68
  return {
66
69
  name: ${nameLiteral},
67
70
  description: ${descriptionLiteral},
@@ -176,51 +179,7 @@ function writeTailwindConfig(dir) {
176
179
  writeFile(path.join(dir, 'tailwind.config.cjs'), `/** @type {import('tailwindcss').Config} */
177
180
  module.exports = {
178
181
  content: ['./src/**/*.{ts,tsx,html}'],
179
- theme: {
180
- extend: {
181
- colors: {
182
- border: 'hsl(var(--border))',
183
- input: 'hsl(var(--input))',
184
- ring: 'hsl(var(--ring))',
185
- background: 'hsl(var(--background))',
186
- foreground: 'hsl(var(--foreground))',
187
- primary: {
188
- DEFAULT: 'hsl(var(--primary))',
189
- foreground: 'hsl(var(--primary-foreground))',
190
- },
191
- secondary: {
192
- DEFAULT: 'hsl(var(--secondary))',
193
- foreground: 'hsl(var(--secondary-foreground))',
194
- },
195
- destructive: {
196
- DEFAULT: 'hsl(var(--destructive))',
197
- foreground: 'hsl(var(--destructive-foreground))',
198
- },
199
- muted: {
200
- DEFAULT: 'hsl(var(--muted))',
201
- foreground: 'hsl(var(--muted-foreground))',
202
- },
203
- accent: {
204
- DEFAULT: 'hsl(var(--accent))',
205
- foreground: 'hsl(var(--accent-foreground))',
206
- },
207
- popover: {
208
- DEFAULT: 'hsl(var(--popover))',
209
- foreground: 'hsl(var(--popover-foreground))',
210
- },
211
- card: {
212
- DEFAULT: 'hsl(var(--card))',
213
- foreground: 'hsl(var(--card-foreground))',
214
- },
215
- },
216
- borderRadius: {
217
- lg: 'var(--radius)',
218
- md: 'calc(var(--radius) - 2px)',
219
- sm: 'calc(var(--radius) - 4px)',
220
- },
221
- },
222
- },
223
- plugins: [],
182
+ presets: [require('@workspace/styling/tailwind')],
224
183
  };
225
184
  `);
226
185
  }
@@ -259,7 +218,7 @@ export default defineBackground(() => {
259
218
  // Open the side panel when the extension icon is clicked.
260
219
  (chrome as ChromeWithSidePanel).sidePanel
261
220
  .setPanelBehavior({ openPanelOnActionClick: true })
262
- .catch(() => {});
221
+ .catch((error) => console.error('Unable to configure the side panel:', error));
263
222
  });
264
223
  `);
265
224
  }
@@ -328,7 +287,6 @@ export function SidePanelApp() {
328
287
  }
329
288
  }, []);
330
289
 
331
-
332
290
  return (
333
291
  <div style={{ padding: 24 }}>
334
292
  <h2>Welcome!</h2>
@@ -351,28 +309,6 @@ function writeStyles(dir) {
351
309
  @tailwind utilities;
352
310
 
353
311
  @layer base {
354
- :root {
355
- --background: 0 0% 100%;
356
- --foreground: 0 0% 9%;
357
- --card: 0 0% 100%;
358
- --card-foreground: 0 0% 9%;
359
- --popover: 0 0% 100%;
360
- --popover-foreground: 0 0% 9%;
361
- --primary: 0 0% 9%;
362
- --primary-foreground: 0 0% 98%;
363
- --secondary: 0 0% 96.1%;
364
- --secondary-foreground: 0 0% 9%;
365
- --muted: 0 0% 96.1%;
366
- --muted-foreground: 0 0% 45.1%;
367
- --accent: 0 0% 96.1%;
368
- --accent-foreground: 0 0% 9%;
369
- --destructive: 0 84.2% 60.2%;
370
- --destructive-foreground: 0 0% 98%;
371
- --border: 0 0% 89.8%;
372
- --input: 0 0% 89.8%;
373
- --ring: 0 0% 63.9%;
374
- --radius: 0.5rem;
375
- }
376
312
 
377
313
  * {
378
314
  border-color: hsl(var(--border));
@@ -1,3 +1,4 @@
1
+ import { NATIVE_ESLINT_VERSION } from '../dependencies.js';
1
2
  import path from 'node:path';
2
3
  import { writeReactLintConfig } from './lint.js';
3
4
  import { appendGitignore, writeFile } from '../util.js';
@@ -62,26 +63,9 @@ module.exports = withNativeWind(config, { input: './global.css', inlineRem: 16 }
62
63
 
63
64
  /** @type {import('tailwindcss').Config} */
64
65
  module.exports = {
65
- darkMode: 'class',
66
66
  content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
67
- presets: [require('nativewind/preset')],
68
- theme: {
69
- extend: {
70
- colors: {
71
- border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))',
72
- background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))',
73
- primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
74
- secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))' },
75
- destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },
76
- muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
77
- accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
78
- popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))' },
79
- card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' },
80
- },
81
- borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' },
82
- borderWidth: { hairline: hairlineWidth() },
83
- },
84
- },
67
+ presets: [require('nativewind/preset'), require('@workspace/styling/tailwind')],
68
+ theme: { extend: { borderWidth: { hairline: hairlineWidth() } } },
85
69
  future: { hoverOnlyWhenSupported: true },
86
70
  plugins: [require('tailwindcss-animate')],
87
71
  };
@@ -89,31 +73,6 @@ module.exports = {
89
73
  writeFile(path.join(dir, 'global.css'), `@tailwind base;
90
74
  @tailwind components;
91
75
  @tailwind utilities;
92
-
93
- @layer base {
94
- :root {
95
- --background: 0 0% 100%; --foreground: 0 0% 3.9%;
96
- --card: 0 0% 100%; --card-foreground: 0 0% 3.9%;
97
- --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%;
98
- --primary: 0 0% 9%; --primary-foreground: 0 0% 98%;
99
- --secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%;
100
- --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%;
101
- --accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%;
102
- --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%;
103
- --border: 0 0% 89.8%; --input: 0 0% 89.8%; --ring: 0 0% 63%; --radius: 0.625rem;
104
- }
105
- .dark:root {
106
- --background: 0 0% 3.9%; --foreground: 0 0% 98%;
107
- --card: 0 0% 3.9%; --card-foreground: 0 0% 98%;
108
- --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%;
109
- --primary: 0 0% 98%; --primary-foreground: 0 0% 9%;
110
- --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%;
111
- --muted: 0 0% 14.9%; --muted-foreground: 0 0% 63.9%;
112
- --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%;
113
- --destructive: 0 70.9% 59.4%; --destructive-foreground: 0 0% 98%;
114
- --border: 0 0% 14.9%; --input: 0 0% 14.9%; --ring: 300 0% 45%;
115
- }
116
- }
117
76
  `);
118
77
  }
119
78
  function expoScheme(projectName) {
@@ -160,11 +119,12 @@ export async function scaffoldExpo({ root, options }) {
160
119
  dependencies: {
161
120
  ...MOBILE_APP_DEPENDENCIES.dependencies,
162
121
  ...MOBILE_NATIVEWIND_DEPENDENCIES.dependencies,
122
+ '@workspace/styling': 'workspace:*',
163
123
  },
164
124
  devDependencies: {
165
125
  ...MOBILE_APP_DEPENDENCIES.devDependencies,
166
126
  '@workspace/eslint-config': 'workspace:*',
167
- eslint: '10.0.3',
127
+ eslint: NATIVE_ESLINT_VERSION,
168
128
  },
169
129
  }, null, 2) + '\n');
170
130
  writeFile(path.join(dir, 'app.json'), JSON.stringify({
@@ -200,7 +160,6 @@ export async function scaffoldExpo({ root, options }) {
200
160
  import { PortalHost } from '@rn-primitives/portal';
201
161
  import { Stack } from 'expo-router';
202
162
 
203
-
204
163
  export default function RootLayout() {
205
164
  return <><Stack /><PortalHost /></>;
206
165
  }
@@ -215,7 +174,7 @@ export default function HomeScreen() {
215
174
  <StatusBar style="auto" />
216
175
  <View style={styles.content}>
217
176
  <Text accessibilityRole="header" style={styles.title}>{${nameLiteral}}</Text>
218
- <Text selectable style={styles.subtitle}>Deterministic Expo application generated by Anhedral.</Text>
177
+ <Text selectable style={styles.subtitle}>Expo application scaffold generated by Anhedral.</Text>
219
178
  <Button onPress={() => void Linking.openURL('https://docs.expo.dev')}>Learn more</Button>
220
179
  </View>
221
180
  </ScrollView>
package/dist/readiness.js CHANGED
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from './bounded-file.js';
2
+ import { existsSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { isDeepStrictEqual } from 'node:util';
4
5
  import { inspectWorkspaceGraph } from './workspace-graph.js';
@@ -8,11 +9,16 @@ import { STANDARD_PRODUCTS } from './stack-registry.js';
8
9
  export function inspectProject(root) {
9
10
  const checks = [];
10
11
  const record = (id, ok, detail) => checks.push({ id, ok, detail });
11
- const read = (file) => JSON.parse(readFileSync(path.join(root, file), 'utf8'));
12
+ const read = (file) => JSON.parse(readBoundedFile(path.join(root, file), MAX_PROJECT_JSON_BYTES).toString('utf8'));
12
13
  record('workspace', existsSync(path.join(root, 'pnpm-workspace.yaml')), 'pnpm workspace manifest');
13
14
  record('lockfile', existsSync(path.join(root, 'pnpm-lock.yaml')), 'Committed dependency resolution required');
14
15
  try {
15
16
  const plan = read('anhedral.setup.json');
17
+ const recordedVersion = plan?.cliVersion;
18
+ const compatible = recordedVersion === CAPABILITY_REGISTRY.cliVersion;
19
+ record('cli-compatibility', compatible, /^\d+\.\d+\.\d+$/.test(recordedVersion ?? '')
20
+ ? `Use anhedral@${recordedVersion} for this project or review upgrade differences`
21
+ : 'Use the recorded project CLI version or review upgrade differences');
16
22
  validateSetupPlan(plan);
17
23
  const standard = read('anhedral.standard.json');
18
24
  record('setup-plan', true, `Requirements recorded by CLI ${plan.cliVersion}`);
@@ -40,7 +46,6 @@ export function inspectProject(root) {
40
46
  catch {
41
47
  record('workspace-graph', false, 'Invalid workspace manifests, references or dependency cycles');
42
48
  }
43
- record('cli-compatibility', plan.cliVersion === CAPABILITY_REGISTRY.cliVersion, 'Use the project CLI version or review upgrade differences');
44
49
  record('selection', standard.schemaVersion === 1 &&
45
50
  JSON.stringify(standard.products) === JSON.stringify(plan.products) &&
46
51
  standard.hosting ===
@@ -68,6 +73,9 @@ export function validateSetupPlan(plan) {
68
73
  value.registryVersion !== 1 ||
69
74
  typeof value.cliVersion !== 'string')
70
75
  throw new Error('Unsupported setup plan version.');
76
+ if (/^\d+\.\d+\.\d+$/.test(value.cliVersion) &&
77
+ value.cliVersion !== CAPABILITY_REGISTRY.cliVersion)
78
+ throw new Error(`Use anhedral@${value.cliVersion} for this project or review upgrade differences.`);
71
79
  if (!Array.isArray(value.products) ||
72
80
  !value.products.length ||
73
81
  !value.products.every((id) => STANDARD_PRODUCTS.includes(id)))
@@ -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';
@@ -22,6 +23,8 @@ export async function addStackItems(root, request, run = execFile) {
22
23
  for (const key of Object.keys(request))
23
24
  if (!['items', 'dryRun', 'skipInstall'].includes(key))
24
25
  throw new Error(`Unknown add option: ${key}. Only bundled registry items are supported.`);
26
+ if (!request.dryRun)
27
+ assertWorkspaceRuntime(run);
25
28
  root = realpathSync(root);
26
29
  if (!request.items.length)
27
30
  throw new Error('Select at least one registry item.');
@@ -62,7 +65,7 @@ export async function addStackItems(root, request, run = execFile) {
62
65
  throw new Error(`Adding this selection relocates ${item.name}; migrate its existing source explicitly.`);
63
66
  }
64
67
  const before = sourceFiles(root);
65
- 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');
66
69
  const commitPaths = [];
67
70
  const conflicts = [];
68
71
  const result = {
@@ -151,7 +154,7 @@ export async function addStackItems(root, request, run = execFile) {
151
154
  commitPaths.push('anhedral.registry.json');
152
155
  // Detect concurrent changes even to source that this addition would otherwise preserve.
153
156
  if (!isDeepStrictEqual(sourceFiles(root), before) ||
154
- readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8') !== receiptText)
157
+ readBoundedFile(path.join(root, 'anhedral.registry.json'), MAX_PROJECT_JSON_BYTES).toString('utf8') !== receiptText)
155
158
  throw new Error('Workspace changed during addition; retry after reviewing source.');
156
159
  },
157
160
  afterCommit: () => {
@@ -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,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[];
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
+ }