anhedral 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +78 -39
  2. package/assets/{anhedral-cli-init-technical.svg → anhedral-stack-technical.svg} +6 -6
  3. package/assets/{anhedral-cli-init.svg → anhedral-stack.svg} +7 -7
  4. package/assets/security/audit-deps.mjs +6 -1
  5. package/dist/bin.js +271 -154
  6. package/dist/bounded-file.d.ts +1 -0
  7. package/dist/bounded-file.js +1 -0
  8. package/dist/capabilities.d.ts +4 -4
  9. package/dist/capabilities.js +1 -1
  10. package/dist/capability-contract.d.ts +47 -0
  11. package/dist/capability-contract.js +136 -0
  12. package/dist/cli-arguments.d.ts +16 -0
  13. package/dist/cli-arguments.js +115 -0
  14. package/dist/cli-output.d.ts +8 -0
  15. package/dist/cli-output.js +211 -0
  16. package/dist/cloudflare.d.ts +7 -2
  17. package/dist/cloudflare.js +166 -36
  18. package/dist/command-schema.d.ts +21 -0
  19. package/dist/command-schema.js +374 -0
  20. package/dist/delivery.d.ts +2 -2
  21. package/dist/delivery.js +21 -4
  22. package/dist/dependencies.d.ts +32 -0
  23. package/dist/dependencies.js +47 -1
  24. package/dist/index.d.ts +23 -1
  25. package/dist/index.js +15 -1
  26. package/dist/inspector-model.d.ts +115 -0
  27. package/dist/inspector-model.js +1 -0
  28. package/dist/lifecycle-model.d.ts +15 -0
  29. package/dist/lifecycle-model.js +20 -0
  30. package/dist/lifecycle.d.ts +12 -0
  31. package/dist/lifecycle.js +92 -0
  32. package/dist/mobile-delivery.d.ts +100 -0
  33. package/dist/mobile-delivery.js +319 -0
  34. package/dist/platforms/desktop.js +4 -5
  35. package/dist/platforms/extension.js +9 -73
  36. package/dist/platforms/mobile.js +14 -52
  37. package/dist/project-inspector.d.ts +4 -0
  38. package/dist/project-inspector.js +646 -0
  39. package/dist/project-state.d.ts +17 -0
  40. package/dist/project-state.js +239 -11
  41. package/dist/provider-auth.d.ts +41 -0
  42. package/dist/provider-auth.js +210 -0
  43. package/dist/providers/adapters.d.ts +17 -0
  44. package/dist/providers/adapters.js +269 -0
  45. package/dist/providers/api.d.ts +28 -0
  46. package/dist/providers/api.js +252 -0
  47. package/dist/providers/config.d.ts +15 -0
  48. package/dist/providers/config.js +184 -0
  49. package/dist/providers/definitions.d.ts +318 -0
  50. package/dist/providers/definitions.js +127 -0
  51. package/dist/providers/provision.d.ts +24 -0
  52. package/dist/providers/provision.js +76 -0
  53. package/dist/providers/receipts.d.ts +20 -0
  54. package/dist/providers/receipts.js +66 -0
  55. package/dist/providers/recovery-verification.d.ts +7 -0
  56. package/dist/providers/recovery-verification.js +112 -0
  57. package/dist/providers/recovery.d.ts +30 -0
  58. package/dist/providers/recovery.js +34 -0
  59. package/dist/providers/runtime.d.ts +10 -0
  60. package/dist/providers/runtime.js +231 -0
  61. package/dist/providers/schema.d.ts +28 -0
  62. package/dist/providers/schema.js +49 -0
  63. package/dist/providers/state.d.ts +56 -0
  64. package/dist/providers/state.js +329 -0
  65. package/dist/provision.d.ts +1 -1
  66. package/dist/provision.js +61 -3
  67. package/dist/readiness.js +11 -3
  68. package/dist/registry/add.d.ts +6 -0
  69. package/dist/registry/add.js +35 -4
  70. package/dist/registry/commands.d.ts +3 -2
  71. package/dist/registry/commands.js +53 -14
  72. package/dist/registry/options.js +3 -2
  73. package/dist/registry/preview.d.ts +11 -0
  74. package/dist/registry/preview.js +91 -0
  75. package/dist/registry/receipt.js +1 -0
  76. package/dist/registry/resolve.js +19 -13
  77. package/dist/registry/types.d.ts +1 -0
  78. package/dist/registry/usage.d.ts +10 -0
  79. package/dist/registry/usage.js +22 -37
  80. package/dist/render.d.ts +2 -0
  81. package/dist/render.js +10 -0
  82. package/dist/scaffold/api.js +20 -3
  83. package/dist/scaffold/auth.js +68 -28
  84. package/dist/scaffold/bootstrap.d.ts +2 -0
  85. package/dist/scaffold/bootstrap.js +9 -0
  86. package/dist/scaffold/database.js +12 -6
  87. package/dist/scaffold/documentation.js +11 -8
  88. package/dist/scaffold/environment.d.ts +2 -0
  89. package/dist/scaffold/environment.js +18 -1
  90. package/dist/scaffold/files.d.ts +1 -0
  91. package/dist/scaffold/files.js +15 -4
  92. package/dist/scaffold/services.js +37 -13
  93. package/dist/scaffold/stack-documentation.d.ts +3 -0
  94. package/dist/scaffold/stack-documentation.js +52 -0
  95. package/dist/scaffold/styling.d.ts +3 -0
  96. package/dist/scaffold/styling.js +84 -0
  97. package/dist/scaffold/web.js +31 -11
  98. package/dist/scaffold/workspace.js +49 -22
  99. package/dist/ship.d.ts +320 -0
  100. package/dist/ship.js +211 -0
  101. package/dist/stack-registry.d.ts +2 -2
  102. package/dist/stack-registry.js +44 -11
  103. package/dist/standard.d.ts +2 -1
  104. package/dist/standard.js +13 -12
  105. package/dist/vercel.d.ts +3 -3
  106. package/dist/vercel.js +27 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/dist/vinext.js +9 -18
  110. package/dist/workspace-graph.js +3 -2
  111. package/docs/application-stack-standard.md +51 -80
  112. package/docs/cli-design.md +67 -0
  113. package/docs/delivery-workflow.md +29 -15
  114. package/docs/plugin-ui.md +61 -0
  115. package/docs/provider-configuration.md +117 -0
  116. package/package.json +16 -12
  117. package/dist/managed-hosting.d.ts +0 -49
  118. package/dist/managed-hosting.js +0 -373
  119. package/docs/managed-hosting-architecture.md +0 -87
  120. package/docs/sponsorships.md +0 -25
package/dist/ship.js ADDED
@@ -0,0 +1,211 @@
1
+ import { existsSync, realpathSync, writeFileSync, rmSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { defaultRunner } from './project-state.js';
6
+ import { withLifecycleOperation } from './lifecycle.js';
7
+ import { planDelivery, shipApplication } from './delivery.js';
8
+ import { planProvisioning, provisionApplication } from './provision.js';
9
+ import { planProviderProvisioning, provisionSelectedProviders } from './providers/provision.js';
10
+ import { applicationRuntime, configuredRuntime, runtimeTargets, RUNTIME_KEYS, } from './providers/runtime.js';
11
+ import { CREDENTIALS } from './providers/api.js';
12
+ import { readApplication } from './project-state.js';
13
+ import { planVercelDelivery, configureVercelEnvironment, shipVercelApplication } from './vercel.js';
14
+ /** Provisioning can change tracked binding IDs; never silently commit that change. */
15
+ async function executeCloudflareApplication(root, dryRun = false, run = defaultRunner, request, send = fetch, providerEnvironment = process.env, session) {
16
+ root = realpathSync(root);
17
+ const deployment = planDelivery(root, true);
18
+ const provisioning = planProvisioning(root);
19
+ const integrations = planProviderProvisioning(root);
20
+ if (dryRun)
21
+ return { ...deployment, provisioning, integrations, mutation: false };
22
+ if (deployment.project.audience === 'unconfigured' || !deployment.project.accountId)
23
+ throw new Error('Connect the intended account and audience before shipping.');
24
+ const environment = runtimeEnvironment(providerEnvironment);
25
+ const providerRun = (command, args, cwd, env) => run(command, args, cwd, runtimeEnvironment(env));
26
+ if (realpathSync((await run('git', ['rev-parse', '--show-toplevel'], root, environment)).trim()) !==
27
+ root ||
28
+ (await run('git', ['status', '--porcelain'], root, environment)).trim())
29
+ throw new Error('Shipping requires a clean application repository before provisioning.');
30
+ const commit = (await run('git', ['rev-parse', 'HEAD'], root, environment)).trim();
31
+ const application = await phase(session, 'application-runtime', () => applicationRuntime(root, providerRun));
32
+ await phase(session, 'checks', () => run('pnpm', ['run', 'check'], root, environment));
33
+ if ((await run('git', ['status', '--porcelain'], root, environment)).trim() ||
34
+ (await run('git', ['rev-parse', 'HEAD'], root, environment)).trim() !== commit)
35
+ throw new Error('Source changed during checks; provisioning was not started.');
36
+ const configured = await phase(session, 'integrations', () => provisionSelectedProviders(root, false, providerRun, send, providerEnvironment));
37
+ if (!configured.ready)
38
+ return { state: 'provider-configuration-required', deployed: false, integrations: configured };
39
+ if ((await run('git', ['status', '--porcelain'], root, environment)).trim() ||
40
+ (await run('git', ['rev-parse', 'HEAD'], root, environment)).trim() !== commit)
41
+ throw new Error('Source changed during provider configuration; provisioning was stopped.');
42
+ const resources = await phase(session, 'resources', () => provisionApplication(root, false, providerRun, request));
43
+ if ((await run('git', ['status', '--porcelain'], root, environment)).trim()) {
44
+ return {
45
+ state: 'configuration-required',
46
+ resources,
47
+ deployed: false,
48
+ nextAction: 'Review and commit the resolved resource bindings, then run anhedral ship again. No source commit or deployment was performed automatically.',
49
+ };
50
+ }
51
+ if ((await run('git', ['rev-parse', 'HEAD'], root, environment)).trim() !== commit)
52
+ throw new Error('Source changed during provider configuration; deployment was not started.');
53
+ const runtime = configuredRuntime(root);
54
+ const environments = mergeRuntimeTargets(runtimeTargets(root, runtime), application);
55
+ const buildRunner = async (command, args, cwd, env) => {
56
+ const values = environments[`${path.relative(root, cwd).split(path.sep).join('/')}/.dev.vars`];
57
+ const environment = runtimeEnvironment(env, values);
58
+ if (command !== 'pnpm' ||
59
+ args.join(' ') !== 'run deploy' ||
60
+ !values ||
61
+ !Object.keys(values).length)
62
+ return run(command, args, cwd, environment);
63
+ const file = path.join(tmpdir(), `anhedral-secrets-${randomUUID()}.json`);
64
+ try {
65
+ writeFileSync(file, JSON.stringify(values), { mode: 0o600, flag: 'wx' });
66
+ return await run(command, [...args, '--secrets-file', file], cwd, environment);
67
+ }
68
+ finally {
69
+ rmSync(file, { force: true });
70
+ }
71
+ };
72
+ return phase(session, 'deployment', () => shipApplication(root, buildRunner, request));
73
+ }
74
+ async function executeVercelApplication(root, dryRun = false, run = defaultRunner, send = fetch, providerEnvironment = process.env, session) {
75
+ const deployment = planVercelDelivery(root);
76
+ const integrations = planProviderProvisioning(root);
77
+ if (dryRun)
78
+ return { ...deployment, integrations, mutation: false };
79
+ if (!deployment.project.projectId ||
80
+ !deployment.project.teamId ||
81
+ deployment.project.audience === 'unconfigured')
82
+ throw new Error('Connect the intended Vercel account, plan and audience before shipping.');
83
+ const env = runtimeEnvironment(providerEnvironment);
84
+ const providerRun = (command, args, cwd, env) => run(command, args, cwd, runtimeEnvironment(env));
85
+ if (realpathSync((await run('git', ['rev-parse', '--show-toplevel'], root, env)).trim()) !==
86
+ realpathSync(root) ||
87
+ (await run('git', ['status', '--porcelain'], root, env)).trim())
88
+ throw new Error('Shipping requires a clean application repository before provisioning.');
89
+ const commit = (await run('git', ['rev-parse', 'HEAD'], root, env)).trim();
90
+ const application = await phase(session, 'application-runtime', () => applicationRuntime(root, providerRun));
91
+ await phase(session, 'checks', () => run('pnpm', ['run', 'check'], root, env));
92
+ if ((await run('git', ['status', '--porcelain'], root, env)).trim() ||
93
+ (await run('git', ['rev-parse', 'HEAD'], root, env)).trim() !== commit)
94
+ throw new Error('Source changed during checks; provisioning was not started.');
95
+ const configured = await phase(session, 'integrations', () => provisionSelectedProviders(root, false, providerRun, send, providerEnvironment));
96
+ if (!configured.ready)
97
+ return { state: 'provider-configuration-required', deployed: false, integrations: configured };
98
+ const runtime = configuredRuntime(root);
99
+ if ((await run('git', ['status', '--porcelain'], root, env)).trim() ||
100
+ (await run('git', ['rev-parse', 'HEAD'], root, env)).trim() !== commit)
101
+ throw new Error('Source changed during provider configuration; deployment was not started.');
102
+ const environments = mergeRuntimeTargets(runtimeTargets(root, runtime), application);
103
+ const values = environments['apps/web/.env.local'] ?? {};
104
+ await phase(session, 'runtime', () => configureVercelEnvironment(root, values, providerRun));
105
+ const buildRunner = (command, args, cwd, env) => run(command, args, cwd, runtimeEnvironment(env, values));
106
+ return phase(session, 'deployment', () => shipVercelApplication(root, buildRunner));
107
+ }
108
+ function phase(session, name, action) {
109
+ return session ? session.phase(name, action) : action();
110
+ }
111
+ export async function shipCloudflareApplication(root, dryRun = false, run = defaultRunner, request, send = fetch, providerEnvironment = process.env) {
112
+ if (dryRun)
113
+ return executeCloudflareApplication(root, true, run, request, send, providerEnvironment);
114
+ return withLifecycleOperation(root, 'cloudflare', run, (session) => executeCloudflareApplication(root, false, run, request, send, providerEnvironment, session));
115
+ }
116
+ export async function shipSelectedVercelApplication(root, dryRun = false, run = defaultRunner, send = fetch, providerEnvironment = process.env) {
117
+ if (dryRun)
118
+ return executeVercelApplication(root, true, run, send, providerEnvironment);
119
+ return withLifecycleOperation(root, 'vercel', run, (session) => executeVercelApplication(root, false, run, send, providerEnvironment, session));
120
+ }
121
+ function runtimeEnvironment(base, values = {}) {
122
+ const environment = { ...base, CI: '1', NO_COLOR: '1' };
123
+ for (const name of Object.values(CREDENTIALS))
124
+ delete environment[name];
125
+ for (const name of RUNTIME_KEYS)
126
+ if (!name.startsWith('NEXT_PUBLIC_') && !name.startsWith('EXPO_PUBLIC_'))
127
+ delete environment[name];
128
+ return { ...environment, ...values };
129
+ }
130
+ function mergeRuntimeTargets(providers, application) {
131
+ const targets = { ...providers };
132
+ for (const [file, values] of Object.entries(application))
133
+ targets[file] = { ...targets[file], ...values };
134
+ return targets;
135
+ }
136
+ /** Each mixed target keeps its own release receipt; this is not a provider transaction. */
137
+ export async function shipSelectedStack(root, dryRun = false, run = defaultRunner, request, send = fetch, providerEnvironment = process.env) {
138
+ root = realpathSync(root);
139
+ const mixed = existsSync(path.join(root, 'anhedral.web.json')) &&
140
+ existsSync(path.join(root, 'anhedral.project.json')) &&
141
+ readApplication(root).targets.length > 0;
142
+ if (!mixed)
143
+ return existsSync(path.join(root, 'anhedral.web.json'))
144
+ ? shipSelectedVercelApplication(root, dryRun, run, send, providerEnvironment)
145
+ : shipCloudflareApplication(root, dryRun, run, request, send, providerEnvironment);
146
+ const cloudflare = planDelivery(root, true);
147
+ const vercel = planVercelDelivery(root);
148
+ if (dryRun)
149
+ return {
150
+ mutation: false,
151
+ dependencies: [
152
+ { target: 'cloudflare', after: [] },
153
+ { target: 'vercel', after: ['cloudflare'] },
154
+ ],
155
+ targets: [
156
+ { provider: 'cloudflare', ...cloudflare },
157
+ { provider: 'vercel', ...vercel },
158
+ ],
159
+ integrations: planProviderProvisioning(root),
160
+ };
161
+ if (!cloudflare.project.accountId ||
162
+ cloudflare.project.audience === 'unconfigured' ||
163
+ !vercel.project.projectId ||
164
+ !vercel.project.teamId ||
165
+ vercel.project.audience === 'unconfigured')
166
+ throw new Error('Connect both intended hosting accounts and audiences before mixed delivery.');
167
+ return withLifecycleOperation(root, 'mixed', run, async (session) => {
168
+ const scoped = (prefix) => ({
169
+ phase: (name, action) => session.phase(`${prefix}-${name}`, action),
170
+ });
171
+ const services = await executeCloudflareApplication(root, false, run, request, send, providerEnvironment, scoped('cloudflare'));
172
+ if ('state' in services &&
173
+ ['configuration-required', 'provider-configuration-required'].includes(services.state))
174
+ return {
175
+ state: services.state,
176
+ deployed: false,
177
+ targets: [{ provider: 'cloudflare', result: services }],
178
+ nextAction: 'Resolve Cloudflare setup and review source changes, then repeat ship. Vercel was not deployed.',
179
+ };
180
+ try {
181
+ const web = await executeVercelApplication(root, false, run, send, providerEnvironment, scoped('vercel'));
182
+ const blocked = 'state' in web && web.state === 'provider-configuration-required';
183
+ return {
184
+ state: blocked ? 'partial' : 'deployed',
185
+ deployed: !blocked,
186
+ targets: [
187
+ { provider: 'cloudflare', result: services },
188
+ { provider: 'vercel', result: web },
189
+ ],
190
+ productVerification: 'required',
191
+ ...(blocked
192
+ ? {
193
+ nextAction: 'Cloudflare completed. Resolve Vercel prerequisites and repeat ship; no resource or data rollback was attempted.',
194
+ }
195
+ : {}),
196
+ };
197
+ }
198
+ catch {
199
+ return {
200
+ state: 'partial',
201
+ deployed: false,
202
+ targets: [
203
+ { provider: 'cloudflare', result: services },
204
+ { provider: 'vercel', state: 'failed' },
205
+ ],
206
+ productVerification: 'required',
207
+ nextAction: 'Cloudflare completed; Vercel stopped. Inspect hosting status and the failed operation phase, then repeat the deployment plan. No resource or data rollback was attempted.',
208
+ };
209
+ }
210
+ });
211
+ }
@@ -30,7 +30,7 @@ declare const PRODUCT_ITEMS: {
30
30
  styling: ItemDefinition;
31
31
  };
32
32
  export type StandardProduct = keyof typeof PRODUCT_ITEMS;
33
- export declare const STANDARD_PRODUCTS: readonly ("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")[];
33
+ export declare const STANDARD_PRODUCTS: readonly ("realtime" | "d1" | "r2" | "kv" | "queues" | "workflows" | "electron" | "wxt" | "expo" | "vinext" | "next" | "styling" | "hono" | "neon" | "local-data" | "clerk" | "better-auth" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog")[];
34
34
  declare const DEFINITIONS: {
35
35
  vinext: ItemDefinition;
36
36
  next: ItemDefinition;
@@ -67,7 +67,7 @@ declare const DEFINITIONS: {
67
67
  export type StackItemId = keyof typeof DEFINITIONS;
68
68
  export declare function getStackDefinition(name: string): ItemDefinition;
69
69
  export declare function getStackItem(name: string): StackRegistryItem;
70
- export declare const STACK_ITEMS: Record<"realtime" | "electron" | "eslint" | "typescript" | "wxt" | "expo" | "next" | "vinext" | "hono" | "neon" | "better-auth" | "queues" | "workflows" | "workspace" | "ui" | "d1" | "local-data" | "clerk" | "r2" | "kv" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog" | "styling", StackRegistryItem>;
70
+ export declare const STACK_ITEMS: Record<"realtime" | "d1" | "r2" | "kv" | "queues" | "workflows" | "electron" | "eslint" | "typescript" | "wxt" | "expo" | "vinext" | "next" | "ui" | "workspace" | "styling" | "hono" | "neon" | "local-data" | "clerk" | "better-auth" | "cron" | "openai" | "ai-sdk" | "workers-ai" | "resend" | "stripe" | "revenuecat" | "sentry" | "basin" | "posthog", StackRegistryItem>;
71
71
  export declare const STACK_REGISTRY: {
72
72
  schemaVersion: number;
73
73
  name: string;
@@ -1,3 +1,5 @@
1
+ import { scaffoldStyling } from './scaffold/styling.js';
2
+ import { resourceName } from './render.js';
1
3
  import { STACK_DEPENDENCIES } from './dependencies.js';
2
4
  import { dependencyOrder } from './dependency-order.js';
3
5
  import { GENERATOR_VERSION } from './version.js';
@@ -55,7 +57,7 @@ const PRODUCT_ITEMS = defineItems({
55
57
  type: 'application',
56
58
  description: 'Expo mobile application with native UI',
57
59
  targets: ['apps/mobile'],
58
- registryDependencies: ['workspace', 'typescript', 'eslint'],
60
+ registryDependencies: ['workspace', 'typescript', 'eslint', 'styling'],
59
61
  setup: requirements('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']),
60
62
  install: scaffoldExpo,
61
63
  },
@@ -71,7 +73,7 @@ const PRODUCT_ITEMS = defineItems({
71
73
  type: 'application',
72
74
  description: 'WXT browser side-panel extension',
73
75
  targets: ['apps/extension'],
74
- registryDependencies: ['workspace', 'typescript', 'eslint'],
76
+ registryDependencies: ['workspace', 'typescript', 'eslint', 'styling'],
75
77
  setup: requirements('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']),
76
78
  install: scaffoldWxt,
77
79
  },
@@ -106,7 +108,7 @@ const PRODUCT_ITEMS = defineItems({
106
108
  d1_databases: [
107
109
  {
108
110
  binding: 'DB',
109
- database_name: `${options.name}-db`,
111
+ database_name: resourceName(options.name, 'db'),
110
112
  database_id: 'REPLACE_WITH_D1_ID',
111
113
  migrations_dir: '../../packages/db/migrations',
112
114
  },
@@ -120,7 +122,12 @@ const PRODUCT_ITEMS = defineItems({
120
122
  targets: ['packages/local-data'],
121
123
  registryDependencies: ['workspace', 'typescript', 'eslint'],
122
124
  setup: requirements('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']),
123
- package: packageSource('local-data', '// Device-local SQLite and filesystem need platform-specific adapters.\nexport interface LocalDataStore { read(key: string): Promise<string | null>; write(key: string, value: string): Promise<void>; }\n', {}),
125
+ package: packageSource('local-data', `// Device-local SQLite and filesystem need platform-specific adapters.
126
+ export interface LocalDataStore {
127
+ read(key: string): Promise<string | null>;
128
+ write(key: string, value: string): Promise<void>;
129
+ }
130
+ `, {}),
124
131
  },
125
132
  clerk: {
126
133
  type: 'integration',
@@ -130,7 +137,17 @@ const PRODUCT_ITEMS = defineItems({
130
137
  registryDependencies: ['workspace', 'typescript', 'eslint'],
131
138
  setup: requirements('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']),
132
139
  install: scaffoldClerk,
133
- environment: ['NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY'],
140
+ environment: [
141
+ 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY',
142
+ 'EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY',
143
+ 'CLERK_PUBLISHABLE_KEY',
144
+ 'CLERK_SECRET_KEY',
145
+ ],
146
+ resolveEnvironment: ({ has }) => [
147
+ 'CLERK_SECRET_KEY',
148
+ ...(has('vinext') || has('next') ? ['NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY'] : []),
149
+ ...(has('hono') ? ['CLERK_PUBLISHABLE_KEY'] : []),
150
+ ],
134
151
  runtime: 'server',
135
152
  resolveDependencies: ({ has }) => ['vinext', 'next', 'hono'].filter((id) => has(id)),
136
153
  resolveTargets: ({ has }) => [
@@ -162,7 +179,7 @@ const PRODUCT_ITEMS = defineItems({
162
179
  ]),
163
180
  install: scaffoldPrivateFiles,
164
181
  configure: ({ options }) => ({
165
- r2_buckets: [{ binding: 'FILES', bucket_name: `${options.name}-private-files` }],
182
+ r2_buckets: [{ binding: 'FILES', bucket_name: resourceName(options.name, 'private-files') }],
166
183
  }),
167
184
  runtime: 'worker',
168
185
  resolveTargets: ({ runtimeApp }) => [`apps/${runtimeApp}/private-files.md`],
@@ -194,7 +211,7 @@ const PRODUCT_ITEMS = defineItems({
194
211
  setup: requirements('Deferred jobs', ['Wrangler'], ['Cloudflare Queues configuration access'], [], ['Queue, producer/consumer bindings and failure handling'], ['Retries, idempotency, concurrency and dead-letter recovery']),
195
212
  install: scaffoldQueues,
196
213
  configure: ({ options }) => ({
197
- queues: { producers: [{ binding: 'JOBS', queue: `${options.name}-jobs` }] },
214
+ queues: { producers: [{ binding: 'JOBS', queue: resourceName(options.name, 'jobs') }] },
198
215
  }),
199
216
  runtime: 'optional-worker',
200
217
  resolveTargets: ({ runtimeApp, has }) => [
@@ -277,6 +294,7 @@ const PRODUCT_ITEMS = defineItems({
277
294
  registryDependencies: ['stripe'],
278
295
  setup: requirements('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']),
279
296
  install: scaffoldRevenueCat,
297
+ environment: ['EXPO_PUBLIC_REVENUECAT_IOS_API_KEY', 'EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY'],
280
298
  },
281
299
  sentry: {
282
300
  type: 'package',
@@ -298,7 +316,9 @@ const PRODUCT_ITEMS = defineItems({
298
316
  'Basin dataset/stream, ANALYTICS binding, optional private R2 sink and authorized sink writes',
299
317
  ], ['Private ingestion, event delivery, query results and archive retrieval when selected']),
300
318
  package: packageSource('analytics', `export interface EventStream { send(events: Record<string, unknown>[]): Promise<void> }
301
- export async function recordEvents(stream: EventStream, events: Record<string, unknown>[]) { await stream.send(events); }
319
+ export async function recordEvents(stream: EventStream, events: Record<string, unknown>[]) {
320
+ await stream.send(events);
321
+ }
302
322
  `, {}),
303
323
  resolvePackage: ({ has }, recipe) => ({
304
324
  ...recipe,
@@ -325,8 +345,21 @@ export async function recordEvents(stream: EventStream, events: Record<string, u
325
345
  description: 'Shared platform-independent styling tokens',
326
346
  targets: ['packages/styling'],
327
347
  registryDependencies: ['workspace', 'typescript', 'eslint'],
328
- setup: requirements('Shared styling definitions', [], [], [], ['Styling package; implement actual shared colors, typography, spacing and themes'], ['Consistency across selected interfaces; preserve platform-specific components']),
329
- package: packageSource('styling', "export const styling = { colors: { foreground: '#171717', background: '#ffffff' }, spacing: { small: 4, medium: 8, large: 16 } } as const;\n", {}),
348
+ setup: requirements('Shared styling definitions', [], [], [], [
349
+ 'Shared light/dark tokens and Tailwind v3 preset for Expo/WXT; adapt platform typography and components',
350
+ ], ['Consistency across selected interfaces; preserve platform-specific components']),
351
+ package: packageSource('styling', `import { light, dark, spacing } from './tokens.cjs';
352
+
353
+ export const styling = {
354
+ colors: {
355
+ foreground: 'hsl(' + light['--foreground'] + ')',
356
+ background: 'hsl(' + light['--background'] + ')',
357
+ },
358
+ spacing,
359
+ themes: { light, dark },
360
+ };
361
+ `, {}),
362
+ install: scaffoldStyling,
330
363
  },
331
364
  });
332
365
  export const STANDARD_PRODUCTS = Object.freeze(Object.keys(PRODUCT_ITEMS));
@@ -366,7 +399,7 @@ function freeze(value) {
366
399
  }
367
400
  const DEFINITIONS = freeze({ ...FOUNDATION_ITEMS, ...PRODUCT_ITEMS });
368
401
  function describe(definition) {
369
- const { install: _install, configure: _configure, resolveTargets: _targets, resolveDependencies: _dependencies, resolvePackage: _package, fallbackDependency: _fallback, ...metadata } = definition;
402
+ const { install: _install, configure: _configure, resolveTargets: _targets, resolveEnvironment: _environment, resolveDependencies: _dependencies, resolvePackage: _package, fallbackDependency: _fallback, ...metadata } = definition;
370
403
  return structuredClone(metadata);
371
404
  }
372
405
  export function getStackDefinition(name) {
@@ -1,7 +1,8 @@
1
1
  import { parseStandardOptions } from './registry/options.js';
2
2
  import { STANDARD_USAGE } from './registry/usage.js';
3
3
  import type { StandardOptions, StandardRunner } from './registry/types.js';
4
+ import { type OutputOptions } from './cli-output.js';
4
5
  export { parseStandardOptions, STANDARD_USAGE };
5
6
  export { STANDARD_PRODUCTS } from './stack-registry.js';
6
7
  export type { StandardOptions, StandardProduct, StandardRunner } from './registry/types.js';
7
- export declare function scaffoldStandardProject(options: StandardOptions, run?: StandardRunner): Promise<void>;
8
+ export declare function scaffoldStandardProject(options: StandardOptions, run?: StandardRunner, output?: OutputOptions): Promise<void>;
package/dist/standard.js CHANGED
@@ -8,19 +8,22 @@ import { registerVercelApplication } from './vercel.js';
8
8
  import { registerApplication } from './delivery.js';
9
9
  import { runStagedTransaction } from './transaction.js';
10
10
  import { createSetupPlan } from './capabilities.js';
11
- import { bootstrapWorkspace, assertBootstrapPlatform } from './scaffold/bootstrap.js';
11
+ import { bootstrapWorkspace, assertBootstrapPlatform, assertWorkspaceRuntime, } from './scaffold/bootstrap.js';
12
12
  import { composeStack } from './scaffold/compose.js';
13
13
  import { parseStandardOptions } from './registry/options.js';
14
14
  import { STANDARD_USAGE } from './registry/usage.js';
15
+ import { printProgress, printResult } from './cli-output.js';
15
16
  export { parseStandardOptions, STANDARD_USAGE };
16
17
  export { STANDARD_PRODUCTS } from './stack-registry.js';
17
- export async function scaffoldStandardProject(options, run = execFile) {
18
+ export async function scaffoldStandardProject(options, run = execFile, output = {}) {
18
19
  const plan = createSetupPlan(options);
20
+ const display = { ...output, json: options.json };
19
21
  if (options.dryRun) {
20
- console.log(options.json ? JSON.stringify(plan, null, 2) : JSON.stringify(plan));
22
+ printResult('init', plan, { ...output, json: options.json });
21
23
  return;
22
24
  }
23
25
  assertBootstrapPlatform(run);
26
+ assertWorkspaceRuntime(run);
24
27
  const root = options.root;
25
28
  const previousQuiet = process.env.ANHEDRAL_QUIET;
26
29
  if (options.json)
@@ -46,12 +49,15 @@ export async function scaffoldStandardProject(options, run = execFile) {
46
49
  throw new Error('Destination changed before generation.');
47
50
  },
48
51
  build: async (stageRoot) => {
52
+ printProgress('Creating the shadcn/Turborepo workspace…', display);
49
53
  bootstrapWorkspace(stageRoot, options.name, run);
54
+ printProgress('Composing the selected stack…', display);
50
55
  await composeStack(stageRoot, options, plan.registry);
51
56
  if (options.products.includes('next') && options.hosting === 'vercel')
52
57
  registerVercelApplication(stageRoot);
53
58
  if (readdirSync(path.join(stageRoot, 'apps')).some((app) => existsSync(path.join(stageRoot, 'apps', app, 'wrangler.jsonc'))))
54
59
  registerApplication(stageRoot);
60
+ printProgress('Resolving pinned dependencies…', display);
55
61
  run('pnpm', ['install', '--lockfile-only', '--no-frozen-lockfile'], stageRoot);
56
62
  if (configureSecurityBackports(stageRoot))
57
63
  run('pnpm', ['install', '--lockfile-only', '--no-frozen-lockfile'], stageRoot);
@@ -67,18 +73,13 @@ export async function scaffoldStandardProject(options, run = execFile) {
67
73
  if (checkout.status !== 0 || realpathSync(checkout.stdout.trim()) !== realpathSync(root))
68
74
  run('git', ['init'], root);
69
75
  }
70
- if (!options.skipInstall)
76
+ if (!options.skipInstall) {
77
+ printProgress('Installing workspace dependencies…', display);
71
78
  run('pnpm', ['install', '--frozen-lockfile'], root);
79
+ }
72
80
  },
73
81
  });
74
- console.log(options.json
75
- ? JSON.stringify({ ...plan, generated: true })
76
- : `Created ${root}. Next: cd ${JSON.stringify(root)}, then pnpm dev. Implement the product flows, configure selected resources using README.md and anhedral.setup.json, and run pnpm check. Record stack decisions in docs/architecture.md and exercised acceptance criteria in docs/verification.md.`);
77
- if (!options.json) {
78
- console.log('\nHelp keep Anhedral Init free and open source.');
79
- console.log('Sponsor ongoing development, maintenance, and documentation.');
80
- console.log('Become a sponsor: https://github.com/anhedral/anhedral-init/blob/main/docs/sponsorships.md');
81
- }
82
+ printResult('init', { ...plan, generated: true }, { ...output, json: options.json });
82
83
  }
83
84
  finally {
84
85
  if (created && existsSync(root) && readdirSync(root).length === 0)
package/dist/vercel.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type DeliveryRunner } from './project-state.js';
2
- interface WebProject {
2
+ export interface WebProject {
3
3
  schemaVersion: 1;
4
4
  id: string;
5
5
  name: string;
@@ -17,7 +17,7 @@ interface WebProject {
17
17
  productionBranch?: string;
18
18
  };
19
19
  }
20
- interface WebRelease {
20
+ export interface WebRelease {
21
21
  id: string;
22
22
  commit: string;
23
23
  operation: 'ship' | 'rollback' | 'adopt' | 'recover';
@@ -73,6 +73,7 @@ export declare function vercelApplicationStatus(root: string, refresh?: boolean,
73
73
  productVerification: string;
74
74
  management: string | null;
75
75
  }>;
76
+ export declare function configureVercelEnvironment(root: string, values: Record<string, string>, run?: DeliveryRunner): Promise<void>;
76
77
  export declare function shipVercelApplication(root: string, run?: DeliveryRunner): Promise<{
77
78
  projectId: string;
78
79
  release: WebRelease;
@@ -95,4 +96,3 @@ export declare function rollbackVercelApplication(root: string, releaseId: strin
95
96
  nextAction: string;
96
97
  }>;
97
98
  export declare function writeVercelOverview(root: string, snapshot: Awaited<ReturnType<typeof vercelApplicationStatus>>): string;
98
- export {};
package/dist/vercel.js CHANGED
@@ -544,7 +544,7 @@ export function planVercelDelivery(root) {
544
544
  checks: ['pnpm run check'],
545
545
  deployment: ['pnpm', ...cli, 'deploy', '--prod', '--yes'],
546
546
  cloudflareServices: existsSync(path.join(root, 'anhedral.project.json'))
547
- ? 'Deploy and verify separately with --provider cloudflare before shipping the web app.'
547
+ ? 'Default ship deploys Cloudflare services first. Explicit --provider vercel ships only the web app; verify its service dependencies separately.'
548
548
  : 'none',
549
549
  mutation: false,
550
550
  };
@@ -575,6 +575,32 @@ export async function vercelApplicationStatus(root, refresh = false, run = defau
575
575
  management: project.scope ? `https://vercel.com/${project.scope}/${project.name}` : null,
576
576
  };
577
577
  }
578
+ export async function configureVercelEnvironment(root, values, run = defaultRunner) {
579
+ const project = readWeb(root);
580
+ if (!project.projectId || !project.teamId)
581
+ throw new Error('Connect Vercel before configuring environment variables.');
582
+ context(root, project);
583
+ await inspect(root, project, run);
584
+ if (!Object.keys(values).length)
585
+ return;
586
+ const response = await api(root, run, `/v10/projects/${project.projectId}/env?upsert=true`, project.teamId, Object.entries(values).map(([key, value]) => ({
587
+ key,
588
+ value,
589
+ type: 'encrypted',
590
+ target: ['production'],
591
+ })));
592
+ const created = Array.isArray(response.created)
593
+ ? response.created
594
+ : response.created
595
+ ? [response.created]
596
+ : [];
597
+ if (!Array.isArray(response.failed) ||
598
+ response.failed.length ||
599
+ Object.keys(values).some((key) => !created.some((entry) => entry.key === key &&
600
+ (entry.target === 'production' ||
601
+ (Array.isArray(entry.target) && entry.target.includes('production'))))))
602
+ throw new Error('Vercel did not confirm all selected production environment variables; deployment was not started.');
603
+ }
578
604
  export async function shipVercelApplication(root, run = defaultRunner) {
579
605
  root = realpathSync(root);
580
606
  const result = await withApplicationLock(root, async () => {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const GENERATOR_VERSION = "0.10.0";
1
+ export declare const GENERATOR_VERSION = "0.11.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const GENERATOR_VERSION = '0.10.0';
1
+ export const GENERATOR_VERSION = '0.11.0';
package/dist/vinext.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { VINEXT_DEPENDENCIES } from './dependencies.js';
1
2
  import { existsSync, readFileSync, rmSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { writeFile } from './util.js';
@@ -8,28 +9,14 @@ export function configureVinext(root, workerName, bindings) {
8
9
  const pkg = JSON.parse(readFileSync(path.join(root, 'apps/web/package.json'), 'utf8'));
9
10
  delete pkg.dependencies?.['@opennextjs/cloudflare'];
10
11
  pkg.type = 'module';
11
- pkg.dependencies = {
12
- ...pkg.dependencies,
13
- vinext: '1.1.0',
14
- next: '16.4.0',
15
- react: '19.2.8',
16
- 'react-dom': '19.2.8',
17
- 'react-server-dom-webpack': '19.2.8',
18
- };
19
- pkg.devDependencies = {
20
- ...pkg.devDependencies,
21
- vite: '8.3.4',
22
- '@cloudflare/vite-plugin': '1.63.1',
23
- '@vitejs/plugin-rsc': '0.5.36',
24
- '@vitejs/plugin-react': '6.1.2',
25
- wrangler: '4.149.0',
26
- };
12
+ pkg.dependencies = { ...pkg.dependencies, ...VINEXT_DEPENDENCIES.dependencies };
13
+ pkg.devDependencies = { ...pkg.devDependencies, ...VINEXT_DEPENDENCIES.devDependencies };
27
14
  pkg.scripts = {
28
15
  ...pkg.scripts,
29
16
  dev: 'vite dev --host 127.0.0.1 --port 3000',
30
17
  build: 'vite build',
31
18
  start: 'vinext start',
32
- preview: 'wrangler dev --config dist/server/wrangler.json --persist-to .wrangler/state',
19
+ preview: 'wrangler dev --config dist/server/wrangler.json --port 3000 --persist-to .wrangler/state',
33
20
  deploy: 'vite build && wrangler deploy --config dist/server/wrangler.json',
34
21
  'build:worker': 'vite build',
35
22
  typecheck: 'wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts && tsc --noEmit',
@@ -77,7 +64,11 @@ export default config;
77
64
  const uiFile = path.join(root, 'packages/ui/package.json');
78
65
  if (existsSync(uiFile)) {
79
66
  const ui = JSON.parse(readFileSync(uiFile, 'utf8'));
80
- ui.dependencies = { ...ui.dependencies, react: '19.2.8', 'react-dom': '19.2.8' };
67
+ ui.dependencies = {
68
+ ...ui.dependencies,
69
+ react: VINEXT_DEPENDENCIES.dependencies.react,
70
+ 'react-dom': VINEXT_DEPENDENCIES.dependencies['react-dom'],
71
+ };
81
72
  writeFile(uiFile, JSON.stringify(ui, null, 2) + '\n');
82
73
  }
83
74
  }
@@ -1,4 +1,5 @@
1
- import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
1
+ import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from './bounded-file.js';
2
+ import { existsSync, lstatSync, readdirSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { dependencyOrder } from './dependency-order.js';
4
5
  const dependencyFields = [
@@ -12,7 +13,7 @@ function readManifest(file, directory) {
12
13
  throw new Error(`Workspace manifest missing: ${directory}.`);
13
14
  if (lstatSync(file).isSymbolicLink())
14
15
  throw new Error(`Workspace symlinks are unsupported: ${directory}/package.json.`);
15
- const value = JSON.parse(readFileSync(file, 'utf8'));
16
+ const value = JSON.parse(readBoundedFile(file, MAX_PROJECT_JSON_BYTES).toString('utf8'));
16
17
  if (!value || typeof value !== 'object' || Array.isArray(value))
17
18
  throw new Error(`Invalid workspace manifest: ${directory}.`);
18
19
  const manifest = value;