anhedral 0.6.1 → 0.7.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.
package/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
  [![npm](https://img.shields.io/npm/v/anhedral)](https://www.npmjs.com/package/anhedral)
5
5
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
6
6
 
7
- Initialize client-owned applications using the [Anhedral Application Stack & Delivery Standard](docs/application-stack-standard.md).
7
+ Anhedral guides the development lifecycle through a ChatGPT/Codex plugin: planning, developer setup, infrastructure, implementation, verification, delivery, and operations. The CLI initializes the repository foundation using the [Anhedral Application Stack & Delivery Standard](docs/application-stack-standard.md).
8
+
9
+ The plugin source lives in `plugins/anhedral`. It guides recommended plugins, CLI tooling, client-owned accounts, infrastructure and scoped permissions for the selected stack. It uses available integrations and documented alternatives; installation does not grant provider access. Install the repository plugin with `codex plugin marketplace add anhedral/anhedral-init`, then `codex plugin add anhedral@anhedral`. Releases include a self-contained plugin ZIP with the same version as the CLI. A public ChatGPT directory listing requires separate submission.
8
10
 
9
11
  Every new project starts from the official **shadcn pnpm/Turborepo monorepo**. The default creates **Next.js + React + TypeScript + Tailwind + shadcn/ui**, hosted on **Cloudflare Workers through OpenNext**. Select additional apps and services only when needed.
10
12
 
@@ -35,9 +37,11 @@ Generated projects include `dev`, `build`, `lint`, `typecheck`, `test`, `audit`,
35
37
 
36
38
  Provider packages and services are **starters**, not finished product features. Generated READMEs identify the remaining setup: resource IDs, migrations, product authorization, webhooks, native adapters and runtime SDK initialization. Initialization does not create cloud resources, activate billing, or deploy. Human approval is required for paid activation, material spending, architecture exceptions and production releases.
37
39
 
38
- The CLI supports `new`, `init`, `--help`, and `--version`. Use `anhedral --help` for options.
40
+ The CLI supports `new`, `init`, `doctor`, `--help`, and `--version`. `anhedral doctor <directory> --json` checks local setup requirements without writes; provider access and production readiness remain unverified. Use `anhedral --help` for options. Its JSON plan includes versioned setup requirements, and generated projects include `anhedral.setup.json`. Requirements do not imply that accounts or resources are configured. The CLI and plugin registry share one source in `src/capabilities.ts`; `pnpm build` refreshes the plugin's bundled registry and stack standard.
41
+
42
+ Requires Node.js 20.19+ in the 20 series or 22.12+ and pnpm 10.34.5. Version 0.6 replaces the legacy generator and builder with the Cloudflare-first initializer; earlier generator commands and templates are retired. Existing applications are not automatically migrated. Pin the CLI version and commit the generated lockfile. The `shadcn@latest` bootstrap can change between generation dates; review upgrades before use.
39
43
 
40
- Requires Node.js 20.19+ in the 20 series or 22.12+ and pnpm 10.34.5. Version 0.6 replaces the legacy generator and builder with the Cloudflare-first initializer; earlier generator commands and templates are retired. Existing applications are not automatically migrated. Pin a CLI version for reproducible initialization and review upgrades before use.
44
+ Initialize from Linux, macOS, or WSL. Native Windows initialization fails before writes because upstream shadcn currently resolves monorepo component paths outside the project; planning and `doctor` remain portable. Expo's release gate currently blocks unpatched `node-forge` and `braces` advisories in its CLI/Metro dependencies. Application releases require resolving or explicitly reviewing those risks; the initializer does not waive them.
41
45
 
42
46
  ## Contributing and security
43
47
 
@@ -59,6 +63,7 @@ scripts/, tests/, docs/ Tooling, verification, and the stack standard
59
63
  Each demo retains its own pnpm workspace and lockfile; the root package remains the initializer. Dependencies, caches, generated artifacts, and local credentials are ignored.
60
64
 
61
65
  ```sh
66
+ pnpm test:integration # Real web, API/auth, and packaged desktop paths
62
67
  pnpm --dir demo/anhedral-factory dev
63
68
  pnpm --dir demo/anhedral-factory check
64
69
  ```
@@ -230,11 +230,11 @@
230
230
  <text x="1539" y="1221" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Workers AI when advantageous</text>
231
231
  <rect x="1435" y="1234" width="430" height="80" rx="12" fill="url(#card-fill)" stroke="#73777b" stroke-width="1.15"/>
232
232
  <rect x="1451" y="1246" width="56" height="56" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
233
- <g transform="translate(1455 1250) scale(2)"><path d="M14.679 0c4.648 0 7.413 2.765 7.413 6.434s-2.765 6.434-7.413 6.434H12.33L24 24h-8.245l-8.88-8.44c-.636-.588-.93-1.273-.93-1.86 0-.831.587-1.565 1.713-1.883l4.574-1.224c1.737-.465 2.936-1.81 2.936-3.572 0-2.153-1.761-3.4-3.939-3.4H0V0z" fill="#f7f5f0"/></g>
233
+ <g transform="translate(1455 1250) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
234
234
  <path d="M 1519 1248 L 1519 1300" fill="none" stroke="#34424e" stroke-width="1" stroke-linecap="round"/>
235
- <text x="1539" y="1265" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Resend + Email Routing</text>
236
- <text x="1539" y="1289" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">Outbound app email + inbound forwarding</text>
237
- <text x="1539" y="1309" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Business mailbox stays with existing provider</text>
235
+ <text x="1539" y="1265" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Domain + Mail</text>
236
+ <text x="1539" y="1289" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">GoDaddy broker · Cloudflare nameservers</text>
237
+ <text x="1539" y="1309" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Cloudflare Routing + Sending / Resend alternative</text>
238
238
  <rect x="1435" y="1322" width="430" height="80" rx="12" fill="url(#card-fill)" stroke="#73777b" stroke-width="1.15"/>
239
239
  <rect x="1451" y="1334" width="56" height="56" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
240
240
  <g transform="translate(1455 1338) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
@@ -230,11 +230,11 @@
230
230
  <text x="1539" y="1221" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Workers AI when advantageous</text>
231
231
  <rect x="1435" y="1234" width="430" height="80" rx="12" fill="url(#card-fill)" stroke="#73777b" stroke-width="1.15"/>
232
232
  <rect x="1451" y="1246" width="56" height="56" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
233
- <g transform="translate(1455 1250) scale(2)"><path d="M14.679 0c4.648 0 7.413 2.765 7.413 6.434s-2.765 6.434-7.413 6.434H12.33L24 24h-8.245l-8.88-8.44c-.636-.588-.93-1.273-.93-1.86 0-.831.587-1.565 1.713-1.883l4.574-1.224c1.737-.465 2.936-1.81 2.936-3.572 0-2.153-1.761-3.4-3.939-3.4H0V0z" fill="#f7f5f0"/></g>
233
+ <g transform="translate(1455 1250) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
234
234
  <path d="M 1519 1248 L 1519 1300" fill="none" stroke="#34424e" stroke-width="1" stroke-linecap="round"/>
235
- <text x="1539" y="1265" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Resend + Email Routing</text>
236
- <text x="1539" y="1289" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">Outbound app email + inbound forwarding</text>
237
- <text x="1539" y="1309" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Business mailbox stays with existing provider</text>
235
+ <text x="1539" y="1265" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Domain + Mail</text>
236
+ <text x="1539" y="1289" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">GoDaddy broker · Cloudflare nameservers</text>
237
+ <text x="1539" y="1309" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Cloudflare Routing + Sending / Resend alternative</text>
238
238
  <rect x="1435" y="1322" width="430" height="80" rx="12" fill="url(#card-fill)" stroke="#73777b" stroke-width="1.15"/>
239
239
  <rect x="1451" y="1334" width="56" height="56" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
240
240
  <g transform="translate(1455 1338) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
package/dist/bin.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { inspectProject } from './readiness.js';
2
4
  import { argv } from 'node:process';
3
5
  import { STANDARD_USAGE, parseStandardOptions, scaffoldStandardProject } from './standard.js';
4
6
  import { GENERATOR_VERSION } from './version.js';
@@ -6,24 +8,41 @@ import { PostCommitError } from './transaction.js';
6
8
  const [command, ...args] = argv.slice(2);
7
9
  const json = args.includes('--json');
8
10
  async function main() {
9
- if (!command || command === '--help' || command === '-h') {
11
+ if ([undefined, '--help', '-h'].includes(command)) {
10
12
  console.log(json ? JSON.stringify({ usage: STANDARD_USAGE }) : STANDARD_USAGE);
11
13
  return;
12
14
  }
13
- if (command === '--version' || command === '-v') {
15
+ if (['--version', '-v'].includes(command)) {
14
16
  console.log(json ? JSON.stringify({ version: GENERATOR_VERSION }) : GENERATOR_VERSION);
15
17
  return;
16
18
  }
19
+ if (command === 'doctor') {
20
+ runDoctor(args);
21
+ return;
22
+ }
17
23
  if (command !== 'new' && command !== 'init') {
18
- throw new Error(`Unknown command: ${command}. Use new, init, --help, or --version.`);
24
+ throw new Error(`Unknown command: ${command}. Use new, init, doctor, --help, or --version.`);
19
25
  }
20
- if (args.includes('--help') || args.includes('-h')) {
26
+ if (args.some((arg) => ['--help', '-h'].includes(arg))) {
21
27
  console.log(json ? JSON.stringify({ usage: STANDARD_USAGE }) : STANDARD_USAGE);
22
28
  return;
23
29
  }
24
- let options;
30
+ const options = readOptions(command, args);
31
+ if (options)
32
+ await scaffoldStandardProject(options);
33
+ }
34
+ function runDoctor(args) {
35
+ const positional = args.filter((arg) => arg !== '--json');
36
+ if (positional.length > 1 || positional.some((arg) => arg.startsWith('-')))
37
+ throw new Error('Usage: anhedral doctor [directory] [--json]');
38
+ const report = inspectProject(path.resolve(positional[0] ?? process.cwd()));
39
+ console.log(JSON.stringify(report, null, json ? 2 : undefined));
40
+ if (!report.localReady)
41
+ process.exitCode = 1;
42
+ }
43
+ function readOptions(command, args) {
25
44
  try {
26
- options = parseStandardOptions(command, args);
45
+ return parseStandardOptions(command, args);
27
46
  }
28
47
  catch (error) {
29
48
  const message = error instanceof Error ? error.message : String(error);
@@ -31,7 +50,6 @@ async function main() {
31
50
  process.exitCode = 1;
32
51
  return;
33
52
  }
34
- await scaffoldStandardProject(options);
35
53
  }
36
54
  try {
37
55
  await main();
@@ -39,7 +57,8 @@ try {
39
57
  catch (error) {
40
58
  const message = error instanceof Error ? error.message : String(error);
41
59
  const code = error instanceof PostCommitError ? 'POST_COMMIT_FAILED'
42
- : command === 'new' || command === 'init' ? 'GENERATION_FAILED' : 'UNKNOWN_COMMAND';
60
+ : command === 'doctor' ? 'INVALID_ARGUMENT'
61
+ : command === 'new' || command === 'init' ? 'GENERATION_FAILED' : 'UNKNOWN_COMMAND';
43
62
  console.error(json ? JSON.stringify({ error: message, code }) : `Error: ${message}`);
44
63
  process.exitCode = 1;
45
64
  }
@@ -0,0 +1,54 @@
1
+ import type { StandardProduct } from './standard-products.js';
2
+ import type { StandardOptions } from './standard.js';
3
+ export interface Selection {
4
+ app?: boolean;
5
+ conflicts?: StandardProduct[];
6
+ requiresAny?: StandardProduct[];
7
+ requiresAll?: StandardProduct[];
8
+ workerBinding?: boolean;
9
+ }
10
+ export interface Capability {
11
+ purpose: string;
12
+ tools: string[];
13
+ access: string[];
14
+ credentials: string[];
15
+ resources: string[];
16
+ verify: string[];
17
+ selection: Selection;
18
+ }
19
+ /** Setup requirements describe unfinished starters; they never grant authority or claim readiness. */
20
+ export declare const CAPABILITIES: Record<StandardProduct, Capability>;
21
+ export declare function resolveStandardProducts(selected: Set<StandardProduct>, hosting: StandardOptions['hosting'], hostingSpecified: boolean): void;
22
+ export declare const CAPABILITY_REGISTRY: {
23
+ schemaVersion: number;
24
+ cliVersion: string;
25
+ capabilities: Record<"next" | "expo" | "electron" | "wxt" | "hono" | "neon" | "d1" | "local-data" | "clerk" | "better-auth" | "r2" | "kv" | "realtime" | "queues" | "cron" | "workflows" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog" | "styling", Capability>;
26
+ };
27
+ export declare function createSetupPlan(options: StandardOptions): {
28
+ schemaVersion: number;
29
+ cliVersion: string;
30
+ registryVersion: number;
31
+ standard: string;
32
+ root: string;
33
+ products: ("next" | "expo" | "electron" | "wxt" | "hono" | "neon" | "d1" | "local-data" | "clerk" | "better-auth" | "r2" | "kv" | "realtime" | "queues" | "cron" | "workflows" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog" | "styling")[];
34
+ hosting: "cloudflare" | "vercel";
35
+ bootstrap: string[];
36
+ dryRun: boolean;
37
+ setup: {
38
+ tools: string[];
39
+ access: string[];
40
+ capabilities: {
41
+ status: "starter" | "planned";
42
+ purpose: string;
43
+ tools: string[];
44
+ access: string[];
45
+ credentials: string[];
46
+ resources: string[];
47
+ verify: string[];
48
+ selection: Selection;
49
+ id: "next" | "expo" | "electron" | "wxt" | "hono" | "neon" | "d1" | "local-data" | "clerk" | "better-auth" | "r2" | "kv" | "realtime" | "queues" | "cron" | "workflows" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog" | "styling";
50
+ }[];
51
+ completion: string[];
52
+ resourcesProvisioned: boolean;
53
+ };
54
+ };
@@ -0,0 +1,90 @@
1
+ import { GENERATOR_VERSION } from './version.js';
2
+ import { PACKAGE_MANAGER } from './dependencies.js';
3
+ function capability(purpose, tools, access, credentials, resources, verify, selection = {}) {
4
+ return { purpose, tools, access, credentials, resources, verify, selection };
5
+ }
6
+ /** Setup requirements describe unfinished starters; they never grant authority or claim readiness. */
7
+ export const CAPABILITIES = {
8
+ next: capability('Web application', ['Browser control'], [], [], ['Next.js app; configure the selected hosting provider'], ['Web flows, accessibility, production hosting build and deployed preview'], { app: true }),
9
+ expo: capability('Mobile application', ['Expo/EAS CLI', 'Xcode or Android SDK'], ['Client-owned Apple/Google developer accounts for store delivery'], ['Platform signing credentials; EAS access when used'], ['Expo app; native adapters, signing, store configuration'], ['Native simulator/device flows and signed native build; web export alone is insufficient'], { app: true }),
10
+ electron: capability('Desktop application', ['Electron tooling', 'Platform packaging tools'], ['Signing/notarization account for the target platform'], ['Signing credentials; notarization credentials when required'], ['Electron app; packaging and distribution configuration'], ['Packaged desktop app, permissions, update/recovery behavior'], { app: true }),
11
+ wxt: capability('Chrome side-panel extension', ['Chrome', 'WXT'], ['Extension store publisher access for distribution'], ['Store publishing credentials when required'], ['Extension app; minimal browser permissions and store package'], ['Installed side panel, background/content messaging and permission behavior'], { app: true }),
12
+ hono: capability('Shared OpenAPI backend', ['Wrangler'], ['Cloudflare Workers deployment access'], [], ['API Worker; application routes and authorization'], ['Contract validation, unauthenticated rejection and resource ownership'], { app: true }),
13
+ neon: capability('PostgreSQL persistence through Drizzle and Hyperdrive', ['Neon integration or API', 'Wrangler'], ['Neon project/branch access; Cloudflare Hyperdrive configuration access'], ['DATABASE_URL for migrations; separate least-privilege Hyperdrive origin credentials'], ['Neon project/branches, database roles, migrations, HYPERDRIVE binding'], ['Migration and runtime connectivity; disable query caching for correctness-sensitive reads'], { conflicts: ['d1'], workerBinding: true }),
14
+ d1: capability('SQLite-compatible hosted persistence', ['Wrangler'], ['Cloudflare D1 access'], [], ['D1 database, DB binding and reviewed migrations'], ['Migration, constraints, transactional behavior and runtime queries'], { conflicts: ['neon'], workerBinding: true }),
15
+ 'local-data': capability('Device-local SQLite and files', ['Selected platform database/filesystem tooling'], [], [], ['Integration boundary; implement platform adapters and explicit synchronization if needed'], ['Persistence, offline writes and recovery on the selected device']),
16
+ clerk: capability('Managed identity', ['Clerk integration or dashboard'], ['Client-owned Clerk project access'], ['Clerk publishable key; server secret key; webhook secret when used'], ['Identity project, allowed URLs, SDK setup and protected routes'], ['Sign-in/out, session expiry, resource authorization and selected native client flows'], { conflicts: ['better-auth'], requiresAny: ['next', 'hono'] }),
17
+ 'better-auth': capability('Application-managed identity', ['Selected database tooling'], ['Selected database migration/runtime access'], ['BETTER_AUTH_SECRET; BETTER_AUTH_URL'], ['Selected Neon or D1 database, auth schema, trusted origins and application routes'], ['Sign-in/out, session expiry, sign-up policy and resource authorization'], { conflicts: ['clerk'], requiresAny: ['neon', 'd1'], workerBinding: true }),
18
+ r2: capability('Private remote files', ['Wrangler'], ['Cloudflare R2 bucket/binding access'], [], ['Private R2 bucket and FILES binding; authorized upload/download endpoints'], ['Public bucket access disabled; file ownership enforced; clients receive no storage credentials'], { workerBinding: true }),
19
+ kv: capability('Eventually consistent cache/configuration', ['Wrangler'], ['Cloudflare KV namespace access'], [], ['KV namespace and CONFIG binding'], ['TTL and propagation behavior; never use KV as transactional authority'], { workerBinding: true }),
20
+ realtime: capability('Rooms and collaboration', ['Wrangler', 'Browser control'], ['Cloudflare Durable Objects deployment access'], [], ['Realtime Worker, Durable Object namespace/migration and WebSocket authorization'], ['Authorized room isolation, disconnect/reconnect and deployed WebSockets']),
21
+ queues: capability('Deferred jobs', ['Wrangler'], ['Cloudflare Queues configuration access'], [], ['Queue, producer/consumer bindings and failure handling'], ['Retries, idempotency, concurrency and dead-letter recovery']),
22
+ cron: capability('Scheduled work', ['Wrangler'], ['Cloudflare Worker deployment access'], [], ['Scheduled Worker and environment-specific Cron Triggers'], ['Schedule, idempotency, failure visibility and overlapping runs']),
23
+ workflows: capability('Durable multi-step processing', ['Wrangler'], ['Cloudflare Workflows deployment access'], [], ['Workflow Worker/binding and resumable steps'], ['Retry/resume, idempotency, cancellation and failure recovery']),
24
+ openai: capability('Direct OpenAI integration', ['OpenAI integration or API'], ['Client-owned OpenAI project and model access'], ['OPENAI_API_KEY'], ['Server SDK factory; implement application behavior and spending limits'], ['Selected model, failure handling, private inputs and cost controls'], { conflicts: ['ai-sdk'] }),
25
+ 'ai-sdk': capability('Streaming AI and provider abstraction', ['Selected AI provider integration or API'], ['Selected provider project/model access'], ['OPENAI_API_KEY for the generated default; selected provider credentials otherwise'], ['Server SDK factory; implement streaming, tools and application behavior'], ['Streaming, tool authorization, interruption and spending limits'], { conflicts: ['openai'] }),
26
+ 'workers-ai': capability('Cloudflare-hosted inference', ['Wrangler'], ['Cloudflare Workers AI model access'], [], ['AI binding and selected model invocation'], ['Supported model behavior, authorization and usage limits'], { workerBinding: true }),
27
+ resend: capability('Outbound application email', ['Resend integration or API'], ['Client-owned Resend project; sending-domain DNS access'], ['RESEND_API_KEY; webhook secret when used'], ['Verified sending domain, sender and server integration'], ['Delivery, suppression/bounce handling and webhook verification; inbound routing is separate']),
28
+ stripe: capability('Web payments and subscriptions', ['Stripe integration or CLI'], ['Client-owned Stripe account; test/live access as required'], ['STRIPE_SECRET_KEY; STRIPE_WEBHOOK_SECRET'], ['Products/prices, signed webhooks and entitlement implementation'], ['Test purchase/cancellation, webhook replay and transactional idempotency']),
29
+ revenuecat: capability('Cross-store subscription entitlements alongside Stripe', ['RevenueCat integration or API', 'Selected mobile store tooling'], ['RevenueCat project; App Store/Play Store product access'], ['Platform public SDK keys; server API/webhook credentials as required'], ['Integration guidance only; install platform SDKs and configure offerings/store products'], ['Sandbox purchase/restore, verified webhooks and entitlement reconciliation'], { requiresAll: ['stripe'] }),
30
+ sentry: capability('Additional runtime/crash diagnostics', ['Sentry integration or API'], ['Client-owned Sentry project access'], ['SENTRY_DSN; source-map upload token when required'], ['Integration boundary; install/init runtime SDKs and configure private source-map uploads'], ['Captured runtime errors, source maps, privacy filtering and alert delivery']),
31
+ basin: capability('Cloudflare analytical events', ['Wrangler', 'Cloudflare integration or API'], ['Cloudflare analytics/Pipelines access; plan supporting selected services; Pipelines Setup grant for managed R2 sink setup'], [], ['Basin dataset/stream, ANALYTICS binding, optional private R2 sink and authorized sink writes'], ['Private ingestion, event delivery, query results and archive retrieval when selected'], { workerBinding: true }),
32
+ posthog: capability('Specialized product analytics', ['PostHog integration or API'], ['Client-owned PostHog project access'], ['POSTHOG_API_KEY; POSTHOG_HOST; separate privileged key if needed'], ['Analytics client factory; implement consent, event schema and privacy filtering'], ['Event receipt, consent behavior and redaction of private data']),
33
+ styling: capability('Shared styling definitions', [], [], [], ['Styling package; implement actual shared colors, typography, spacing and themes'], ['Consistency across selected interfaces; preserve platform-specific components']),
34
+ };
35
+ export function resolveStandardProducts(selected, hosting, hostingSpecified) {
36
+ if (![...selected].some((id) => CAPABILITIES[id].selection.app))
37
+ selected.add('next');
38
+ for (const id of selected)
39
+ validateSelection(id, selected);
40
+ const needsWorker = [...selected].some((id) => CAPABILITIES[id].selection.workerBinding);
41
+ if (needsWorker && !selected.has('hono') && (!selected.has('next') || hosting !== 'cloudflare')) {
42
+ throw new Error('Selected server bindings need a Cloudflare Next.js app or an explicitly selected Hono Worker.');
43
+ }
44
+ if (hostingSpecified && !selected.has('next'))
45
+ throw new Error('--hosting applies to the Next.js app.');
46
+ }
47
+ function validateSelection(id, selected) {
48
+ const { conflicts = [], requiresAny = [], requiresAll = [] } = CAPABILITIES[id].selection;
49
+ const conflict = conflicts.find((other) => selected.has(other));
50
+ if (conflict)
51
+ throw new Error(`Choose one of ${[id, conflict].join(' / ')}.`);
52
+ if (requiresAny.length && !requiresAny.some((other) => selected.has(other))) {
53
+ throw new Error(`${id} requires an explicitly selected ${requiresAny.join(' or ')} capability.`);
54
+ }
55
+ const missing = requiresAll.filter((other) => !selected.has(other));
56
+ if (missing.length)
57
+ throw new Error(`${id} requires ${missing.join(', ')}; select it explicitly.`);
58
+ }
59
+ export const CAPABILITY_REGISTRY = { schemaVersion: 1, cliVersion: GENERATOR_VERSION, capabilities: CAPABILITIES };
60
+ const HOSTING_SETUP = {
61
+ cloudflare: {
62
+ tools: ['Cloudflare integration or API', 'Wrangler'],
63
+ access: ['Client-owned Cloudflare account; account ID; scoped OAuth/API access; service-compatible plan; DNS access only when needed'],
64
+ },
65
+ vercel: {
66
+ tools: ['Vercel integration or CLI'],
67
+ access: ['Client-owned Vercel project access; approved architecture exception'],
68
+ },
69
+ };
70
+ export function createSetupPlan(options) {
71
+ const capabilities = options.products.map((id) => ({ id, ...CAPABILITIES[id], status: options.dryRun ? 'planned' : 'starter' }));
72
+ const providers = [...new Set([
73
+ ...(options.products.includes('next') ? [options.hosting] : []),
74
+ ...capabilities.flatMap(({ tools }) => tools.includes('Wrangler') ? ['cloudflare'] : []),
75
+ ])].map((provider) => HOSTING_SETUP[provider]);
76
+ return {
77
+ schemaVersion: 1, cliVersion: GENERATOR_VERSION, registryVersion: CAPABILITY_REGISTRY.schemaVersion,
78
+ standard: 'anhedral-application-stack', root: options.root, products: options.products, hosting: options.hosting,
79
+ bootstrap: ['pnpm', 'dlx', 'shadcn@latest', 'init', '--monorepo', '--template', 'next'], dryRun: options.dryRun,
80
+ setup: {
81
+ tools: [...new Set(['Git', 'Node.js 22.13+ for generated workspaces', PACKAGE_MANAGER.replace('@', ' '), 'GitHub integration or gh', 'Fallow', 'Cloudflare security audit skill',
82
+ ...providers.flatMap((provider) => provider.tools), ...capabilities.flatMap((item) => item.tools)])],
83
+ access: ['Client-owned repository and CI access', ...providers.flatMap((provider) => provider.access)],
84
+ capabilities,
85
+ completion: ['Discover available plugins and fallbacks; verify the selected account and scope', 'Configure separate environments, bindings and server-side secrets',
86
+ 'Implement starter behavior and meaningful tests', 'Run pnpm check plus platform/provider verification', 'Verify deployed preview; obtain required production authorization; record rollback and operating ownership'],
87
+ resourcesProvisioned: false,
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,5 @@
1
+ /** Windows batch shims cannot be spawned without a shell; invoke their Node entry instead. */
2
+ export declare function resolveCommand(command: string, args: readonly string[], platform?: NodeJS.Platform, searchPath?: string): {
3
+ command: string;
4
+ args: string[];
5
+ };
@@ -0,0 +1,16 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Windows batch shims cannot be spawned without a shell; invoke their Node entry instead. */
4
+ export function resolveCommand(command, args, platform = process.platform, searchPath = process.env.PATH ?? '') {
5
+ if (platform !== 'win32' || !['pnpm', 'pnpm.cmd'].includes(command))
6
+ return { command, args: [...args] };
7
+ const directories = searchPath.split(';').filter(Boolean);
8
+ const executable = directories.map((directory) => path.join(directory, 'pnpm.exe')).find(existsSync);
9
+ if (executable)
10
+ return { command: executable, args: [...args] };
11
+ const entries = ['node_modules/pnpm/bin/pnpm.cjs', 'node_modules/corepack/dist/pnpm.js', 'pnpm.cjs', '../pnpm/bin/pnpm.cjs'];
12
+ const entry = directories.flatMap((directory) => entries.map((file) => path.join(directory, file))).find(existsSync);
13
+ if (!entry)
14
+ throw new Error('Cannot locate a direct pnpm entry point. Install pnpm or enable Corepack.');
15
+ return { command: process.execPath, args: [entry, ...args] };
16
+ }
@@ -24,8 +24,7 @@ export const DESKTOP_DEPENDENCIES = {
24
24
  "electron": '43.7.8',
25
25
  // renovate: datasource=npm depName=electron-builder
26
26
  "electron-builder": '26.15.3',
27
- // renovate: datasource=npm depName=electron-builder-squirrel-windows
28
- "electron-builder-squirrel-windows": '26.15.3',
27
+ "eslint": '10.0.3',
29
28
  // renovate: datasource=npm depName=tailwindcss
30
29
  "tailwindcss": '4.1.18',
31
30
  // renovate: datasource=npm depName=postcss
@@ -74,10 +73,6 @@ export const MOBILE_APP_DEPENDENCIES = {
74
73
  "dependencies": {
75
74
  // renovate: datasource=npm depName=@rn-primitives/portal
76
75
  "@rn-primitives/portal": '1.5.2',
77
- // renovate: datasource=npm depName=@rn-primitives/slot
78
- "@rn-primitives/slot": '1.5.2',
79
- // renovate: datasource=npm depName=class-variance-authority
80
- "class-variance-authority": '0.7.1',
81
76
  // renovate: datasource=npm depName=clsx
82
77
  "clsx": '2.1.1',
83
78
  // renovate: datasource=npm depName=expo
@@ -88,18 +83,12 @@ export const MOBILE_APP_DEPENDENCIES = {
88
83
  "expo-router": '57.0.8',
89
84
  // renovate: datasource=npm depName=expo-status-bar
90
85
  "expo-status-bar": '57.0.1',
91
- // renovate: datasource=npm depName=expo-system-ui
92
- "expo-system-ui": '57.0.1',
93
- // renovate: datasource=npm depName=lucide-react-native
94
- "lucide-react-native": '1.21.0',
95
86
  // renovate: datasource=npm depName=react
96
87
  "react": '19.2.3',
97
88
  // renovate: datasource=npm depName=react-dom
98
89
  "react-dom": '19.2.3',
99
90
  // renovate: datasource=npm depName=react-native
100
91
  "react-native": '0.86.0',
101
- // renovate: datasource=npm depName=react-native-gesture-handler
102
- "react-native-gesture-handler": '2.32.0',
103
92
  // renovate: datasource=npm depName=react-native-reanimated
104
93
  "react-native-reanimated": '4.5.0',
105
94
  // renovate: datasource=npm depName=react-native-safe-area-context
@@ -110,14 +99,16 @@ export const MOBILE_APP_DEPENDENCIES = {
110
99
  "react-native-worklets": '0.10.0',
111
100
  // renovate: datasource=npm depName=react-native-web
112
101
  "react-native-web": '0.21.2',
113
- // renovate: datasource=npm depName=react-native-svg
114
- "react-native-svg": '15.15.4',
115
102
  // renovate: datasource=npm depName=tailwind-merge
116
103
  "tailwind-merge": '3.5.0',
117
104
  // renovate: datasource=npm depName=tailwindcss-animate
118
105
  "tailwindcss-animate": '1.0.7'
119
106
  },
120
107
  "devDependencies": {
108
+ // renovate: datasource=npm depName=@babel/core
109
+ "@babel/core": '7.29.7',
110
+ // renovate: datasource=npm depName=babel-preset-expo
111
+ "babel-preset-expo": '57.0.14',
121
112
  // renovate: datasource=npm depName=@testing-library/dom
122
113
  "@testing-library/dom": '10.4.1',
123
114
  // renovate: datasource=npm depName=@react-native/metro-config
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { STANDARD_PRODUCTS, STANDARD_USAGE, parseStandardOptions, applyStandardOverlays, scaffoldStandardProject, } from './standard.js';
2
2
  export type { StandardOptions, StandardProduct, StandardRunner } from './standard.js';
3
3
  export { GENERATOR_VERSION } from './version.js';
4
+ export { CAPABILITIES, CAPABILITY_REGISTRY, createSetupPlan } from './capabilities.js';
5
+ export type { Capability } from './capabilities.js';
6
+ export { inspectProject, validateSetupPlan } from './readiness.js';
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export { STANDARD_PRODUCTS, STANDARD_USAGE, parseStandardOptions, applyStandardOverlays, scaffoldStandardProject, } from './standard.js';
2
2
  export { GENERATOR_VERSION } from './version.js';
3
+ export { CAPABILITIES, CAPABILITY_REGISTRY, createSetupPlan } from './capabilities.js';
4
+ export { inspectProject, validateSetupPlan } from './readiness.js';
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { writeReactLintConfig } from './lint.js';
2
3
  import { writeFile } from '../util.js';
3
4
  import { DESKTOP_DEPENDENCIES } from '../dependencies.js';
4
5
  import { childPackageName, htmlText, identifierSegment, jsString } from '../render.js';
@@ -6,6 +7,7 @@ export async function scaffoldDesktop(root, options) {
6
7
  const { projectName, displayName } = options;
7
8
  const dir = path.join(root, 'apps/desktop');
8
9
  writePackageJson(dir, projectName);
10
+ writeReactLintConfig(dir);
9
11
  writeTsConfig(dir);
10
12
  writeViteConfig(dir);
11
13
  writePostcssConfig(dir);
@@ -15,7 +17,6 @@ export async function scaffoldDesktop(root, options) {
15
17
  writeSourceFiles(dir, displayName);
16
18
  }
17
19
  function writePackageJson(dir, projectName) {
18
- const updatePublish = {};
19
20
  writeFile(path.join(dir, 'package.json'), JSON.stringify({
20
21
  name: childPackageName(projectName, 'desktop'),
21
22
  version: '0.1.0',
@@ -25,12 +26,12 @@ function writePackageJson(dir, projectName) {
25
26
  scripts: {
26
27
  dev: 'tsc -p tsconfig.main.json && node scripts/dev.mjs',
27
28
  build: 'tsc --noEmit && tsc -p tsconfig.main.json && vite build',
28
- typecheck: 'tsc --noEmit',
29
- 'build:mac': `pnpm build && electron-builder --mac${''}`,
30
- 'build:win': `pnpm build && electron-builder --win${''}`,
31
- 'build:linux': `pnpm build && electron-builder --linux${''}`,
32
- package: `pnpm build && electron-builder${''}`,
33
- ...({}),
29
+ typecheck: 'tsc --noEmit && tsc -p tsconfig.main.json --noEmit',
30
+ lint: 'eslint src --max-warnings 0',
31
+ 'build:mac': 'pnpm build && electron-builder --mac',
32
+ 'build:win': 'pnpm build && electron-builder --win',
33
+ 'build:linux': 'pnpm build && electron-builder --linux',
34
+ package: 'pnpm build && electron-builder',
34
35
  },
35
36
  build: {
36
37
  appId: `dev.anhedral.${identifierSegment(projectName)}`,
@@ -52,13 +53,19 @@ function writePackageJson(dir, projectName) {
52
53
  executableName: identifierSegment(projectName),
53
54
  target: ['AppImage', 'deb'],
54
55
  },
55
- ...updatePublish,
56
56
  },
57
57
  dependencies: DESKTOP_DEPENDENCIES.dependencies,
58
- devDependencies: DESKTOP_DEPENDENCIES.devDependencies,
58
+ devDependencies: { ...DESKTOP_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*' },
59
59
  }, null, 2) + '\n');
60
60
  }
61
61
  function writeTsConfig(dir) {
62
+ writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
63
+ compilerOptions: { target: 'ES2022', lib: ['ES2022', 'DOM'], module: 'ESNext',
64
+ moduleResolution: 'Bundler', jsx: 'react-jsx', strict: true, skipLibCheck: true,
65
+ esModuleInterop: true, noEmit: true, types: ['node', 'vite/client'],
66
+ paths: { '@/*': ['./src/renderer/*'] } },
67
+ include: ['src/renderer/**/*', 'vite.config.ts'],
68
+ }, null, 2) + '\n');
62
69
  writeFile(path.join(dir, 'tsconfig.main.json'), JSON.stringify({
63
70
  compilerOptions: {
64
71
  target: 'ES2022',
@@ -135,16 +142,16 @@ function stop(child) {
135
142
  if (child && child.exitCode === null && !child.killed) child.kill('SIGTERM');
136
143
  }
137
144
 
145
+ async function isServerReady(url) {
146
+ try { return (await fetch(url)).ok; }
147
+ catch { return false; }
148
+ }
149
+
138
150
  async function waitForServer(server, url, timeoutMs = 30_000) {
139
151
  const deadline = Date.now() + timeoutMs;
140
152
  while (Date.now() < deadline) {
141
153
  if (server.exitCode !== null) throw new Error('Vite exited before the dev server was ready.');
142
- try {
143
- const response = await fetch(url);
144
- if (response.ok) return;
145
- } catch {
146
- // The server is still starting.
147
- }
154
+ if (await isServerReady(url)) return;
148
155
  await new Promise((resolve) => setTimeout(resolve, 150));
149
156
  }
150
157
  throw new Error('Timed out waiting for Vite at ' + url + '.');
@@ -344,6 +351,7 @@ export function Button({ className, type = 'button', ...props }: React.ButtonHTM
344
351
  `);
345
352
  writeFile(path.join(dir, 'src/renderer/main.tsx'), `import React from 'react';
346
353
  import ReactDOM from 'react-dom/client';
354
+ import { Button } from '@/components/ui/button';
347
355
  import './styles.css';
348
356
 
349
357
  function App() {
@@ -355,10 +363,7 @@ function App() {
355
363
  ${"Electron + shadcn/ui desktop client ready for your application."}
356
364
  </p>
357
365
  </section>
358
-
359
-
360
-
361
-
366
+ <Button className="self-start" onClick={() => window.location.reload()}>Reload</Button>
362
367
  </main>
363
368
  );
364
369
  }