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.
- package/README.md +78 -39
- package/assets/{anhedral-cli-init-technical.svg → anhedral-stack-technical.svg} +6 -6
- package/assets/{anhedral-cli-init.svg → anhedral-stack.svg} +7 -7
- package/assets/security/audit-deps.mjs +6 -1
- package/dist/bin.js +271 -154
- package/dist/bounded-file.d.ts +1 -0
- package/dist/bounded-file.js +1 -0
- package/dist/capabilities.d.ts +4 -4
- package/dist/capabilities.js +1 -1
- package/dist/capability-contract.d.ts +47 -0
- package/dist/capability-contract.js +136 -0
- package/dist/cli-arguments.d.ts +16 -0
- package/dist/cli-arguments.js +115 -0
- package/dist/cli-output.d.ts +8 -0
- package/dist/cli-output.js +211 -0
- package/dist/cloudflare.d.ts +7 -2
- package/dist/cloudflare.js +166 -36
- package/dist/command-schema.d.ts +21 -0
- package/dist/command-schema.js +374 -0
- package/dist/delivery.d.ts +2 -2
- package/dist/delivery.js +21 -4
- package/dist/dependencies.d.ts +32 -0
- package/dist/dependencies.js +47 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +15 -1
- package/dist/inspector-model.d.ts +115 -0
- package/dist/inspector-model.js +1 -0
- package/dist/lifecycle-model.d.ts +15 -0
- package/dist/lifecycle-model.js +20 -0
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.js +92 -0
- package/dist/mobile-delivery.d.ts +100 -0
- package/dist/mobile-delivery.js +319 -0
- package/dist/platforms/desktop.js +4 -5
- package/dist/platforms/extension.js +9 -73
- package/dist/platforms/mobile.js +14 -52
- package/dist/project-inspector.d.ts +4 -0
- package/dist/project-inspector.js +646 -0
- package/dist/project-state.d.ts +17 -0
- package/dist/project-state.js +239 -11
- package/dist/provider-auth.d.ts +41 -0
- package/dist/provider-auth.js +210 -0
- package/dist/providers/adapters.d.ts +17 -0
- package/dist/providers/adapters.js +269 -0
- package/dist/providers/api.d.ts +28 -0
- package/dist/providers/api.js +252 -0
- package/dist/providers/config.d.ts +15 -0
- package/dist/providers/config.js +184 -0
- package/dist/providers/definitions.d.ts +318 -0
- package/dist/providers/definitions.js +127 -0
- package/dist/providers/provision.d.ts +24 -0
- package/dist/providers/provision.js +76 -0
- package/dist/providers/receipts.d.ts +20 -0
- package/dist/providers/receipts.js +66 -0
- package/dist/providers/recovery-verification.d.ts +7 -0
- package/dist/providers/recovery-verification.js +112 -0
- package/dist/providers/recovery.d.ts +30 -0
- package/dist/providers/recovery.js +34 -0
- package/dist/providers/runtime.d.ts +10 -0
- package/dist/providers/runtime.js +231 -0
- package/dist/providers/schema.d.ts +28 -0
- package/dist/providers/schema.js +49 -0
- package/dist/providers/state.d.ts +56 -0
- package/dist/providers/state.js +329 -0
- package/dist/provision.d.ts +1 -1
- package/dist/provision.js +61 -3
- package/dist/readiness.js +11 -3
- package/dist/registry/add.d.ts +6 -0
- package/dist/registry/add.js +35 -4
- package/dist/registry/commands.d.ts +3 -2
- package/dist/registry/commands.js +53 -14
- package/dist/registry/options.js +3 -2
- package/dist/registry/preview.d.ts +11 -0
- package/dist/registry/preview.js +91 -0
- package/dist/registry/receipt.js +1 -0
- package/dist/registry/resolve.js +19 -13
- package/dist/registry/types.d.ts +1 -0
- package/dist/registry/usage.d.ts +10 -0
- package/dist/registry/usage.js +22 -37
- package/dist/render.d.ts +2 -0
- package/dist/render.js +10 -0
- package/dist/scaffold/api.js +20 -3
- package/dist/scaffold/auth.js +68 -28
- package/dist/scaffold/bootstrap.d.ts +2 -0
- package/dist/scaffold/bootstrap.js +9 -0
- package/dist/scaffold/database.js +12 -6
- package/dist/scaffold/documentation.js +11 -8
- package/dist/scaffold/environment.d.ts +2 -0
- package/dist/scaffold/environment.js +18 -1
- package/dist/scaffold/files.d.ts +1 -0
- package/dist/scaffold/files.js +15 -4
- package/dist/scaffold/services.js +37 -13
- package/dist/scaffold/stack-documentation.d.ts +3 -0
- package/dist/scaffold/stack-documentation.js +52 -0
- package/dist/scaffold/styling.d.ts +3 -0
- package/dist/scaffold/styling.js +84 -0
- package/dist/scaffold/web.js +31 -11
- package/dist/scaffold/workspace.js +49 -22
- package/dist/ship.d.ts +320 -0
- package/dist/ship.js +211 -0
- package/dist/stack-registry.d.ts +2 -2
- package/dist/stack-registry.js +44 -11
- package/dist/standard.d.ts +2 -1
- package/dist/standard.js +13 -12
- package/dist/vercel.d.ts +3 -3
- package/dist/vercel.js +27 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vinext.js +9 -18
- package/dist/workspace-graph.js +3 -2
- package/docs/application-stack-standard.md +51 -80
- package/docs/cli-design.md +67 -0
- package/docs/delivery-workflow.md +29 -15
- package/docs/plugin-ui.md +61 -0
- package/docs/provider-configuration.md +117 -0
- package/package.json +16 -12
- package/dist/managed-hosting.d.ts +0 -49
- package/dist/managed-hosting.js +0 -373
- package/docs/managed-hosting-architecture.md +0 -87
- package/docs/sponsorships.md +0 -25
package/dist/scaffold/auth.js
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
|
+
import { writeEnvironmentFile } from './environment.js';
|
|
1
2
|
import { STACK_DEPENDENCIES } from '../dependencies.js';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { writeBetterAuthSchema } from '../auth-schema.js';
|
|
5
|
-
import {
|
|
6
|
+
import { put, patchPackage, sharedPackage } from './files.js';
|
|
6
7
|
export function scaffoldBetterAuth({ root, options, runtimeApp, has }) {
|
|
7
8
|
writeBetterAuthSchema(root, has('neon'));
|
|
8
9
|
sharedPackage(root, 'auth', `import { betterAuth } from 'better-auth';
|
|
9
10
|
import { drizzleAdapter } from '@better-auth/drizzle-adapter';
|
|
10
11
|
import type { createDatabase } from '@workspace/db';
|
|
11
12
|
export function createAuth(db: ReturnType<typeof createDatabase>, secret: string, baseURL: string) {
|
|
12
|
-
return betterAuth({
|
|
13
|
+
return betterAuth({
|
|
14
|
+
database: drizzleAdapter(db, { provider: '${has('neon') ? 'pg' : 'sqlite'}', transaction: false }),
|
|
15
|
+
secret,
|
|
16
|
+
baseURL,
|
|
17
|
+
emailAndPassword: { enabled: true },
|
|
18
|
+
});
|
|
13
19
|
}
|
|
14
20
|
`, {
|
|
15
|
-
'better-auth': '
|
|
16
|
-
'@better-auth/drizzle-adapter': '
|
|
21
|
+
'better-auth': STACK_DEPENDENCIES['better-auth'],
|
|
22
|
+
'@better-auth/drizzle-adapter': STACK_DEPENDENCIES['@better-auth/drizzle-adapter'],
|
|
17
23
|
'@workspace/db': 'workspace:*',
|
|
18
24
|
});
|
|
19
25
|
patchPackage(root, `apps/${runtimeApp}/package.json`, {
|
|
20
|
-
dependencies: { '@workspace/auth': 'workspace:*' },
|
|
26
|
+
dependencies: { '@workspace/auth': 'workspace:*', '@workspace/db': 'workspace:*' },
|
|
21
27
|
});
|
|
22
28
|
const connection = has('neon') ? 'createDatabase(env.DATABASE_URL)' : 'createDatabase(env.DB)';
|
|
23
29
|
if (has('hono')) {
|
|
24
30
|
const source = readFileSync(path.join(root, 'apps/api/src/index.ts'), 'utf8');
|
|
25
31
|
put(root, 'apps/api/src/index.ts', `import { createAuth } from '@workspace/auth';\nimport { createDatabase } from '@workspace/db';\n` +
|
|
26
32
|
source.replace('export default app;', `app.on(['GET', 'POST'], '/api/auth/*', async (c) => {\n const env = c.env;\n const database = ${connection};\n return await createAuth(database, env.BETTER_AUTH_SECRET, env.BETTER_AUTH_URL).handler(c.req.raw);\n});\nexport default app;`));
|
|
27
|
-
const config = readJson(root, 'apps/api/wrangler.jsonc');
|
|
28
|
-
json(root, 'apps/api/wrangler.jsonc', {
|
|
29
|
-
...config,
|
|
30
|
-
vars: { BETTER_AUTH_URL: 'http://localhost:8787' },
|
|
31
|
-
});
|
|
32
33
|
}
|
|
33
34
|
else {
|
|
34
35
|
put(root, 'apps/web/app/api/auth/[...all]/route.ts', `${has('vinext') ? "import { env } from 'cloudflare:workers';" : options.hosting === 'cloudflare' ? "import { getCloudflareContext } from '@opennextjs/cloudflare';" : ''}
|
|
@@ -44,20 +45,19 @@ export { handler as GET, handler as POST };
|
|
|
44
45
|
if (options.hosting === 'cloudflare' && !has('vinext'))
|
|
45
46
|
put(root, 'apps/web/cloudflare-env.d.ts', `interface CloudflareEnv { BETTER_AUTH_SECRET: string; BETTER_AUTH_URL: string; ${has('neon') ? 'DATABASE_URL: string;' : "DB: Parameters<typeof import('drizzle-orm/d1').drizzle>[0];"} }\n`);
|
|
46
47
|
if (has('vinext')) {
|
|
47
|
-
put(root, 'apps/web/secrets.d.ts', 'declare namespace Cloudflare { interface Env { BETTER_AUTH_SECRET: string } }\n');
|
|
48
|
-
const config = readJson(root, 'apps/web/wrangler.jsonc');
|
|
49
|
-
json(root, 'apps/web/wrangler.jsonc', {
|
|
50
|
-
...config,
|
|
51
|
-
vars: { BETTER_AUTH_URL: 'http://localhost:3000' },
|
|
52
|
-
});
|
|
48
|
+
put(root, 'apps/web/secrets.d.ts', 'declare namespace Cloudflare { interface Env { BETTER_AUTH_SECRET: string; BETTER_AUTH_URL: string } }\n');
|
|
53
49
|
}
|
|
54
50
|
patchPackage(root, 'apps/web/package.json', {
|
|
55
51
|
dependencies: { 'drizzle-orm': STACK_DEPENDENCIES['drizzle-orm'] },
|
|
56
52
|
});
|
|
57
53
|
}
|
|
58
|
-
|
|
54
|
+
writeEnvironmentFile(root, `apps/${runtimeApp}/${runtimeApp === 'web' && options.hosting === 'vercel' ? '.env.example' : '.dev.vars.example'}`, {
|
|
55
|
+
...(has('neon') ? { DATABASE_URL: '' } : {}),
|
|
56
|
+
BETTER_AUTH_SECRET: '',
|
|
57
|
+
BETTER_AUTH_URL: `http://localhost:${runtimeApp === 'web' ? '3000' : '8787'}`,
|
|
58
|
+
});
|
|
59
59
|
if (has('hono'))
|
|
60
|
-
put(root, 'apps/api/src/secrets.d.ts', 'interface Env { BETTER_AUTH_SECRET: string }\n');
|
|
60
|
+
put(root, 'apps/api/src/secrets.d.ts', 'interface Env { BETTER_AUTH_SECRET: string; BETTER_AUTH_URL: string }\n');
|
|
61
61
|
}
|
|
62
62
|
export function scaffoldClerk({ root, has }) {
|
|
63
63
|
if (has('next') || has('vinext')) {
|
|
@@ -74,22 +74,62 @@ export function scaffoldClerk({ root, has }) {
|
|
|
74
74
|
put(root, 'apps/web/app/layout.tsx', 'import { ClerkProvider } from "@clerk/nextjs";\n' + wrapped);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
put(root, 'apps/web/app/sign-in/[[...sign-in]]/page.tsx',
|
|
78
|
-
|
|
77
|
+
put(root, 'apps/web/app/sign-in/[[...sign-in]]/page.tsx', `import { Suspense } from 'react';
|
|
78
|
+
import { SignIn } from '@clerk/nextjs';
|
|
79
|
+
|
|
80
|
+
export default function SignInPage() {
|
|
81
|
+
return (
|
|
82
|
+
<Suspense fallback={<p role="status">Loading sign in…</p>}>
|
|
83
|
+
<SignIn />
|
|
84
|
+
</Suspense>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
`);
|
|
88
|
+
put(root, 'apps/web/app/sign-up/[[...sign-up]]/page.tsx', `import { Suspense } from 'react';
|
|
89
|
+
import { SignUp } from '@clerk/nextjs';
|
|
90
|
+
|
|
91
|
+
export default function SignUpPage() {
|
|
92
|
+
return (
|
|
93
|
+
<Suspense fallback={<p role="status">Loading sign up…</p>}>
|
|
94
|
+
<SignUp />
|
|
95
|
+
</Suspense>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
`);
|
|
79
99
|
put(root, 'apps/web/proxy.ts', `import { clerkMiddleware } from '@clerk/nextjs/server';
|
|
80
|
-
|
|
100
|
+
import type { NextMiddleware } from 'next/server';
|
|
101
|
+
|
|
102
|
+
const proxy: NextMiddleware = clerkMiddleware();
|
|
103
|
+
export default proxy;
|
|
81
104
|
export const config = { matcher: ['/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico)).*)', '/(api|trpc)(.*)'] };
|
|
82
105
|
`);
|
|
83
106
|
// Middleware makes auth available; product routes must explicitly require identity and authorization.
|
|
84
107
|
}
|
|
85
108
|
if (has('hono')) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
sharedPackage(root, 'auth', `import { createClerkClient } from '@clerk/backend';
|
|
110
|
+
|
|
111
|
+
export async function authenticate(
|
|
112
|
+
request: Request,
|
|
113
|
+
secretKey: string,
|
|
114
|
+
publishableKey: string,
|
|
115
|
+
authorizedParties: string[],
|
|
116
|
+
) {
|
|
117
|
+
if (!secretKey || !publishableKey || authorizedParties.length === 0) {
|
|
118
|
+
throw new Error('Configure Clerk keys and explicit authorized origins before authenticating.');
|
|
119
|
+
}
|
|
120
|
+
return createClerkClient({ secretKey, publishableKey }).authenticateRequest(request, {
|
|
121
|
+
authorizedParties,
|
|
122
|
+
});
|
|
92
123
|
}
|
|
93
|
-
|
|
124
|
+
`, { '@clerk/backend': STACK_DEPENDENCIES['@clerk/backend'] });
|
|
125
|
+
writeEnvironmentFile(root, 'apps/api/.dev.vars.example', {
|
|
126
|
+
CLERK_SECRET_KEY: '',
|
|
127
|
+
CLERK_PUBLISHABLE_KEY: '',
|
|
128
|
+
});
|
|
94
129
|
}
|
|
130
|
+
if (has('next') || has('vinext'))
|
|
131
|
+
writeEnvironmentFile(root, 'apps/web/.env.example', {
|
|
132
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: '',
|
|
133
|
+
CLERK_SECRET_KEY: '',
|
|
134
|
+
});
|
|
95
135
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { StandardRunner } from '../registry/types.js';
|
|
2
2
|
export declare function assertBootstrapPlatform(run: StandardRunner, platform?: NodeJS.Platform): void;
|
|
3
|
+
/** Planning and diagnostics support older Node releases; generated dependencies do not. */
|
|
4
|
+
export declare function assertWorkspaceRuntime(run: StandardRunner, version?: string): void;
|
|
3
5
|
export declare function bootstrapWorkspace(root: string, name: string, run: StandardRunner, webOnly?: boolean): void;
|
|
@@ -6,8 +6,17 @@ export function assertBootstrapPlatform(run, platform = process.platform) {
|
|
|
6
6
|
if (platform === 'win32' && run === execFile)
|
|
7
7
|
throw new Error('Native Windows initialization is blocked because the upstream shadcn monorepo bootstrap writes component files outside the project. Run Anhedral inside WSL. Planning and doctor remain available on Windows.');
|
|
8
8
|
}
|
|
9
|
+
/** Planning and diagnostics support older Node releases; generated dependencies do not. */
|
|
10
|
+
export function assertWorkspaceRuntime(run, version = process.versions.node) {
|
|
11
|
+
if (run !== execFile)
|
|
12
|
+
return;
|
|
13
|
+
const [major, minor] = version.split('.').map(Number);
|
|
14
|
+
if (!((major === 22 && minor >= 13) || major >= 24))
|
|
15
|
+
throw new Error('Generated workspaces require Node.js 22.13+ on the 22.x line or Node.js 24+. Planning and doctor remain available on older CLI-supported versions.');
|
|
16
|
+
}
|
|
9
17
|
export function bootstrapWorkspace(root, name, run, webOnly = false) {
|
|
10
18
|
assertBootstrapPlatform(run);
|
|
19
|
+
assertWorkspaceRuntime(run);
|
|
11
20
|
const temporary = path.join(root, '.bootstrap');
|
|
12
21
|
mkdirSync(temporary);
|
|
13
22
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { writeEnvironmentFile } from './environment.js';
|
|
1
2
|
import { STACK_DEPENDENCIES } from '../dependencies.js';
|
|
2
3
|
import { put, patchPackage, sharedPackage } from './files.js';
|
|
3
4
|
export function scaffoldDatabase({ root, options, runtimeApp, has }) {
|
|
@@ -15,11 +16,14 @@ import * as schema from './schema';
|
|
|
15
16
|
export function createDatabase(binding: Parameters<typeof drizzle>[0]) { return drizzle(binding, { schema }); }
|
|
16
17
|
`, {
|
|
17
18
|
'drizzle-orm': STACK_DEPENDENCIES['drizzle-orm'],
|
|
18
|
-
...(has('neon')
|
|
19
|
+
...(has('neon')
|
|
20
|
+
? { '@neondatabase/serverless': STACK_DEPENDENCIES['@neondatabase/serverless'] }
|
|
21
|
+
: {}),
|
|
19
22
|
});
|
|
20
23
|
if (has('neon')) {
|
|
21
24
|
const native = runtimeApp === 'web' && options.hosting === 'vercel';
|
|
22
|
-
|
|
25
|
+
writeEnvironmentFile(root, `apps/${runtimeApp}/${native ? '.env.example' : '.dev.vars.example'}`, { DATABASE_URL: '' });
|
|
26
|
+
writeEnvironmentFile(root, 'packages/db/.env.example', { DATABASE_URL: '' });
|
|
23
27
|
if (!native)
|
|
24
28
|
put(root, `apps/${runtimeApp}/${runtimeApp === 'api' ? 'src/' : ''}database-secrets.d.ts`, runtimeApp === 'api'
|
|
25
29
|
? 'interface Env { DATABASE_URL: string }\n'
|
|
@@ -27,7 +31,12 @@ export function createDatabase(binding: Parameters<typeof drizzle>[0]) { return
|
|
|
27
31
|
}
|
|
28
32
|
put(root, 'packages/db/src/schema.ts', '// Define product tables here; generate and review SQL before migration.\nexport {};\n');
|
|
29
33
|
put(root, 'packages/db/drizzle.config.ts', has('neon')
|
|
30
|
-
? `import {
|
|
34
|
+
? `import { existsSync } from 'node:fs';
|
|
35
|
+
import { loadEnvFile } from 'node:process';
|
|
36
|
+
import { defineConfig } from 'drizzle-kit';
|
|
37
|
+
|
|
38
|
+
if (existsSync('.env')) loadEnvFile('.env');
|
|
39
|
+
|
|
31
40
|
export default defineConfig({ dialect: 'postgresql', schema: './src/schema.ts', out: './migrations', dbCredentials: { url: process.env.DATABASE_URL! } });
|
|
32
41
|
`
|
|
33
42
|
: `import { defineConfig } from 'drizzle-kit';
|
|
@@ -37,7 +46,4 @@ export default defineConfig({ dialect: 'sqlite', schema: './src/schema.ts', out:
|
|
|
37
46
|
devDependencies: { 'drizzle-kit': STACK_DEPENDENCIES['drizzle-kit'] },
|
|
38
47
|
scripts: { 'db:generate': 'drizzle-kit generate' },
|
|
39
48
|
});
|
|
40
|
-
patchPackage(root, `apps/${runtimeApp}/package.json`, {
|
|
41
|
-
dependencies: { '@workspace/db': 'workspace:*' },
|
|
42
|
-
});
|
|
43
49
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { markdownHeading } from '../render.js';
|
|
1
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { put } from './files.js';
|
|
@@ -9,13 +10,15 @@ export function workerBuildSuffix(options) {
|
|
|
9
10
|
export function writeSetupDocumentation(root, options) {
|
|
10
11
|
const has = (product) => options.products.includes(product);
|
|
11
12
|
const agentsFile = path.join(root, 'AGENTS.md');
|
|
13
|
+
const workerSelected = options.products.some((product) => ['vinext', 'hono', 'realtime', 'queues', 'cron', 'workflows'].includes(product)) ||
|
|
14
|
+
(has('next') && options.hosting === 'cloudflare');
|
|
12
15
|
const existingAgents = existsSync(agentsFile) && (has('next') || has('vinext'))
|
|
13
16
|
? readFileSync(agentsFile, 'utf8') + '\n'
|
|
14
17
|
: '';
|
|
15
18
|
put(root, 'AGENTS.md', existingAgents +
|
|
16
19
|
`# Anhedral delivery standard
|
|
17
20
|
|
|
18
|
-
Keep accounts, source, infrastructure, billing, and recovery client-owned. Use
|
|
21
|
+
Keep accounts, source, infrastructure, billing, and recovery client-owned. Use the available developer tools and the selected platforms’ native toolchains for implementation and verification. Use provider plugins, APIs/MCP and CLIs for operations; Wrangler handles Cloudflare project commands.
|
|
19
22
|
|
|
20
23
|
Vinext on Cloudflare Workers by default; native Next.js on Vercel is an explicit option. Free-to-start where suitable, within provider eligibility and limits. Respect the existing account, budget, audience and release authorization. Request only missing approval for paid activation, material spending, architecture changes or release. Do not deploy or activate providers as part of initialization.
|
|
21
24
|
|
|
@@ -27,33 +30,33 @@ Developer tooling includes Cloudflare’s security-audit skill (https://github.c
|
|
|
27
30
|
|
|
28
31
|
For new projects, guide GitHub sign-in and the user's Git identity. Review files and secret ignores, commit the initial source, then use anhedral repository --create for a private repository under the authenticated user. Preserve existing origins and visibility; use an explicitly selected organization when requested. Connect Vercel with --git and verify actual GitHub installation access. For Git delivery, commit and push reviewed changes, wait for production, then use anhedral adopt and anhedral verify. Adoption checks exact repository, production branch and commit without uploading again. Use anhedral ship only for an explicit CLI delivery. Recover a fresh clone with anhedral recover; rerun product verification because prior test evidence is not recovered. Use anhedral doctor --providers to inspect installed tools and recorded provider context.
|
|
29
32
|
|
|
30
|
-
Maintain docs/architecture.md with selected components, rationale, alternatives, data/access boundaries, and a diagram of actual connections. Maintain docs/verification.md with product acceptance criteria, exercised flows, evidence, and blockers. Generated setup is a starter, not implemented product behavior.
|
|
33
|
+
Consult docs/stack.md for the current generated options, targets and setup requirements. Maintain docs/architecture.md with selected components, rationale, alternatives, data/access boundaries, and a diagram of actual connections. Maintain docs/verification.md with product acceptance criteria, exercised flows, evidence, and blockers. Generated setup is a starter, not implemented product behavior.
|
|
31
34
|
|
|
32
35
|
Run pnpm check. Use pnpm exec fallow during development and pnpm exec fallow audit for changes. Fix legitimate findings; use narrow documented exceptions. Verify real UI flows with Computer Use and Control Chrome, plus relevant integrations, native apps/simulators, and deployed previews. Carry an authorized build-and-ship request through deployment and live verification; ask only for authorization that is still missing.
|
|
33
36
|
`);
|
|
34
|
-
put(root, 'README.md', `# ${options.name}
|
|
37
|
+
put(root, 'README.md', `# ${markdownHeading(options.name)}
|
|
35
38
|
|
|
36
39
|
Initialized from the official shadcn pnpm/Turborepo monorepo, with the Anhedral Application Stack & Delivery Standard.
|
|
37
40
|
|
|
38
|
-
Initial selection: ${options.products.join(', ')}. Current selections and additions are recorded in anhedral.setup.json
|
|
41
|
+
Initial selection: ${options.products.join(', ')}. Current selections and additions are recorded in anhedral.setup.json, anhedral.registry.json and [docs/stack.md](docs/stack.md). ${has('vinext') ? 'Vinext App Router on Cloudflare Workers.' : has('next') ? `Next.js hosting: ${options.hosting === 'cloudflare' ? 'Cloudflare Workers + OpenNext' : 'Vercel (native Next.js default)'}.` : ''}
|
|
39
42
|
|
|
40
43
|
Run pnpm install, commit pnpm-lock.yaml, then pnpm dev. Run pnpm check for lint, types, Fallow audit, tests and build. Without a Git comparison base, audit runs the full Fallow gate. Add meaningful product tests and integration checks as behavior is implemented. The tests task runs only workspaces that define tests; init does not claim product verification.
|
|
41
44
|
|
|
42
|
-
${has('next') && options.hosting === 'vercel' ? 'Native Next.js uses customer-owned Vercel. Connect with the intended scope and personal/commercial use before shipping. Default to private all-deployment protection unless public access was requested; verify protection before linking Git or uploading code. Run anhedral doctor --providers for prerequisites and recorded provider context. Configure server secrets in that project; use cache components, partial prefetching, Link navigation and Suspense deliberately. ' : ''}${
|
|
45
|
+
${has('next') && options.hosting === 'vercel' ? 'Native Next.js uses customer-owned Vercel. Connect with the intended scope and personal/commercial use before shipping. Default to private all-deployment protection unless public access was requested; verify protection before linking Git or uploading code. Run anhedral doctor --providers for prerequisites and recorded provider context. Configure server secrets in that project; use cache components, partial prefetching, Link navigation and Suspense deliberately. ' : ''}${workerSelected ? 'Cloudflare bindings contain REPLACE_WITH_* IDs. Reconcile resources in the client account and generate Worker types before development. Workers expose local dev and explicit deploy scripts. ' : ''}${has('vinext') ? 'Build with pnpm build, then pnpm --dir apps/web preview to exercise the production Worker. Vinext provides a Next-compatible API surface; native Next.js is an explicit framework option. ' : ''}${has('next') && options.hosting === 'cloudflare' ? 'Use the web preview script to exercise the actual OpenNext Worker runtime. ' : ''}Initialization does not provision infrastructure or deploy.
|
|
43
46
|
|
|
44
|
-
Security gate: pnpm audit:deps audits all dependencies. Reviewed backports in scripts/security are accepted only after patch hashes, installed package hashes and regression checks match.
|
|
47
|
+
Security gate: pnpm audit:deps audits all dependencies and retains the original report. Reviewed backports in scripts/security are accepted only after patch hashes, installed package hashes and regression checks match. Unremediated high/critical advisories, incomplete reports and modified patches remain blocking; lower-severity findings remain visible in the report.
|
|
45
48
|
|
|
46
49
|
${has('next') && options.hosting === 'cloudflare' ? 'OpenNext is configured without a provisioned incremental-cache bucket. Choose and configure a cache adapter before using ISR or cache features that require shared persistence.\n' : ''}
|
|
47
50
|
${has('neon') ? 'Configure server-only DATABASE_URL for the intended Neon branch and role. Direct HTTP needs no Hyperdrive or connection cleanup. Review migrations and authorization; interactive transactions require a reviewed WebSocket or PostgreSQL adapter.\n' : ''}
|
|
48
51
|
${has('better-auth') ? 'Core Better Auth tables are included in the Drizzle schema. Review and apply migrations before using /api/auth. Configure BETTER_AUTH_SECRET, BETTER_AUTH_URL and trusted origins for the correct environment.\n' : ''}
|
|
49
|
-
${has('clerk') ? '
|
|
52
|
+
${has('clerk') ? `${has('next') || has('vinext') ? 'The web starter includes ClerkProvider, middleware and sign-in/sign-up pages. Configure NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and server-only CLERK_SECRET_KEY. ' : ''}${has('hono') ? 'packages/auth exports a Clerk request authentication helper. Configure its server secret and publishable key, supply explicit authorized origins, and wire it into product routes; the API health route remains public. ' : ''}Authentication does not establish resource authorization.\n` : ''}
|
|
50
53
|
${has('r2') ? 'FILES is a private R2 binding. Implement authentication and file-level authorization before exposing file routes. Clients never receive R2 credentials.\n' : ''}
|
|
51
54
|
${has('realtime') ? 'Durable Object rooms support hibernating WebSockets; the public entrypoint remains disabled until room authorization and short-lived tickets are implemented.\n' : ''}
|
|
52
55
|
${has('queues') ? 'Queue processing retries unimplemented jobs and uses a dead-letter queue. Add product processing and transactional idempotency before acknowledging messages.\n' : ''}
|
|
53
56
|
${has('basin') ? 'Basin uses a Pipelines stream binding. Configure its stream, sink, R2/catalog and SQL in client-owned infrastructure; it is distinct from operational logs.\n' : ''}
|
|
54
57
|
Provider packages are SDK factories or integration boundaries, not finished business features. Connect only needed packages to server entrypoints. Billing needs verified webhooks and idempotent consumption; RevenueCat adds cross-store entitlement configuration. Local data needs a platform-specific SQLite/filesystem adapter. Sentry needs a platform SDK and runtime initialization. Native auth and subscriptions need platform-specific setup.
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
When the product needs a domain, purchase it in the client's GoDaddy account, then add its zone to the client's Cloudflare account and configure Cloudflare nameservers. The client retains registration, renewal, billing and recovery ownership. Verify DNS records and zone activation before connecting application domains and mail. Inbound mail uses Cloudflare Email Routing. Outbound business and application mail defaults to Cloudflare Email Sending; Resend is an alternative when selected. Configure the selected mail provider before use; init does not provision domains or mail services. GitHub Actions runs PR checks and never deploys automatically. Respect existing authorization; request only missing approval for paid activation, material spending, architecture changes or release.
|
|
57
60
|
`);
|
|
58
61
|
put(root, '.github/workflows/check.yml', `name: Check
|
|
59
62
|
on:
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { ResolvedStackRegistry } from '../registry/resolve.js';
|
|
2
|
+
/** Extend examples without discarding earlier recipe defaults or customer comments. */
|
|
3
|
+
export declare function writeEnvironmentFile(root: string, file: string, variables: Record<string, string>): void;
|
|
2
4
|
export declare function writeEnvironmentExample(root: string, registry: ResolvedStackRegistry): void;
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { put } from './files.js';
|
|
4
|
+
/** Extend examples without discarding earlier recipe defaults or customer comments. */
|
|
5
|
+
export function writeEnvironmentFile(root, file, variables) {
|
|
6
|
+
const destination = path.join(root, file);
|
|
7
|
+
let content = existsSync(destination) ? readFileSync(destination, 'utf8') : '';
|
|
8
|
+
const keys = new Set([...content.matchAll(/^[ \t]*(?:export[ \t]+)?([A-Z][A-Z0-9_]*)[ \t]*=/gm)].map((match) => match[1]));
|
|
9
|
+
const additions = Object.entries(variables)
|
|
10
|
+
.filter(([key]) => !keys.has(key))
|
|
11
|
+
.map(([key, value]) => `${key}=${value}`);
|
|
12
|
+
if (additions.length) {
|
|
13
|
+
if (content && !content.endsWith('\n'))
|
|
14
|
+
content += '\n';
|
|
15
|
+
content += additions.join('\n') + '\n';
|
|
16
|
+
}
|
|
17
|
+
put(root, file, content || '\n');
|
|
18
|
+
}
|
|
2
19
|
export function writeEnvironmentExample(root, registry) {
|
|
3
20
|
const names = [...new Set(registry.items.flatMap((item) => item.environment))].sort();
|
|
4
|
-
|
|
21
|
+
writeEnvironmentFile(root, '.env.example', Object.fromEntries(names.map((name) => [name, ''])));
|
|
5
22
|
}
|
package/dist/scaffold/files.d.ts
CHANGED
package/dist/scaffold/files.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { STACK_DEPENDENCIES } from '../dependencies.js';
|
|
2
|
-
import {
|
|
1
|
+
import { STACK_DEPENDENCIES, WORKER_DEV_PORTS } from '../dependencies.js';
|
|
2
|
+
import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from '../bounded-file.js';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { writeFile } from '../util.js';
|
|
5
5
|
export function readJson(root, file) {
|
|
6
|
-
|
|
6
|
+
let value;
|
|
7
|
+
try {
|
|
8
|
+
value = JSON.parse(readBoundedFile(path.join(root, file), MAX_PROJECT_JSON_BYTES).toString('utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error instanceof SyntaxError)
|
|
12
|
+
throw new Error(`Invalid JSON in ${file}.`);
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
7
15
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
16
|
throw new Error(`Expected a JSON object: ${file}.`);
|
|
9
17
|
return value;
|
|
@@ -34,12 +42,15 @@ export function workerConfig(name) {
|
|
|
34
42
|
};
|
|
35
43
|
}
|
|
36
44
|
export function worker(root, app, config, source, dependencies = {}) {
|
|
45
|
+
const ports = WORKER_DEV_PORTS[app];
|
|
46
|
+
if (!ports)
|
|
47
|
+
throw new Error(`Unknown Worker application: ${app}.`);
|
|
37
48
|
json(root, `apps/${app}/package.json`, {
|
|
38
49
|
name: `@workspace/${app}`,
|
|
39
50
|
private: true,
|
|
40
51
|
type: 'module',
|
|
41
52
|
scripts: {
|
|
42
|
-
dev:
|
|
53
|
+
dev: `wrangler dev --port ${ports.http} --inspector-port ${ports.inspector}`,
|
|
43
54
|
build: 'wrangler deploy --dry-run --outdir dist',
|
|
44
55
|
deploy: 'wrangler deploy',
|
|
45
56
|
typecheck: 'wrangler types && tsc --noEmit',
|
|
@@ -1,20 +1,35 @@
|
|
|
1
|
+
import { resourceName } from '../render.js';
|
|
1
2
|
import { workerConfig, worker } from './files.js';
|
|
2
3
|
export function scaffoldRealtime({ root, options }) {
|
|
3
4
|
worker(root, 'realtime', {
|
|
4
|
-
...workerConfig(
|
|
5
|
+
...workerConfig(resourceName(options.name, 'realtime')),
|
|
5
6
|
durable_objects: { bindings: [{ name: 'ROOMS', class_name: 'Room' }] },
|
|
6
7
|
migrations: [{ tag: 'v1', new_sqlite_classes: ['Room'] }],
|
|
7
8
|
}, `import { DurableObject } from 'cloudflare:workers';
|
|
8
9
|
export class Room extends DurableObject<Env> {
|
|
9
10
|
async fetch(request: Request): Promise<Response> {
|
|
10
|
-
if (request.headers.get('Upgrade') !== 'websocket')
|
|
11
|
+
if (request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') {
|
|
12
|
+
return new Response('WebSocket required', { status: 426 });
|
|
13
|
+
}
|
|
11
14
|
const pair = new WebSocketPair();
|
|
12
15
|
this.ctx.acceptWebSocket(pair[1]);
|
|
13
16
|
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
14
17
|
}
|
|
15
18
|
webSocketMessage(sender: WebSocket, message: string | ArrayBuffer) {
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
const size = typeof message === 'string'
|
|
20
|
+
? new TextEncoder().encode(message).byteLength
|
|
21
|
+
: message.byteLength;
|
|
22
|
+
if (size > 16384) {
|
|
23
|
+
sender.close(1009, 'Message too large');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.#broadcastToPeers(sender, message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#broadcastToPeers(sender: WebSocket, message: string | ArrayBuffer) {
|
|
30
|
+
for (const socket of this.ctx.getWebSockets()) {
|
|
31
|
+
if (socket !== sender) socket.send(message);
|
|
32
|
+
}
|
|
18
33
|
}
|
|
19
34
|
}
|
|
20
35
|
export default {
|
|
@@ -28,15 +43,15 @@ export default {
|
|
|
28
43
|
}
|
|
29
44
|
export function scaffoldQueues({ root, options }) {
|
|
30
45
|
worker(root, 'jobs', {
|
|
31
|
-
...workerConfig(
|
|
46
|
+
...workerConfig(resourceName(options.name, 'jobs')),
|
|
32
47
|
queues: {
|
|
33
|
-
producers: [{ binding: 'JOBS', queue:
|
|
48
|
+
producers: [{ binding: 'JOBS', queue: resourceName(options.name, 'jobs') }],
|
|
34
49
|
consumers: [
|
|
35
50
|
{
|
|
36
|
-
queue:
|
|
51
|
+
queue: resourceName(options.name, 'jobs'),
|
|
37
52
|
max_batch_size: 10,
|
|
38
53
|
max_retries: 3,
|
|
39
|
-
dead_letter_queue:
|
|
54
|
+
dead_letter_queue: resourceName(options.name, 'jobs-dlq'),
|
|
40
55
|
},
|
|
41
56
|
],
|
|
42
57
|
},
|
|
@@ -52,7 +67,10 @@ export function scaffoldQueues({ root, options }) {
|
|
|
52
67
|
`);
|
|
53
68
|
}
|
|
54
69
|
export function scaffoldCron({ root, options }) {
|
|
55
|
-
worker(root, 'scheduled', {
|
|
70
|
+
worker(root, 'scheduled', {
|
|
71
|
+
...workerConfig(resourceName(options.name, 'scheduled')),
|
|
72
|
+
triggers: { crons: ['0 * * * *'] },
|
|
73
|
+
}, `export default {
|
|
56
74
|
async scheduled(controller: ScheduledController): Promise<void> {
|
|
57
75
|
// Define a product schedule, idempotency key, overlap policy, and failure alerts before release.
|
|
58
76
|
console.log('Scheduled trigger', controller.scheduledTime);
|
|
@@ -62,10 +80,10 @@ export function scaffoldCron({ root, options }) {
|
|
|
62
80
|
}
|
|
63
81
|
export function scaffoldWorkflows({ root, options }) {
|
|
64
82
|
worker(root, 'workflows', {
|
|
65
|
-
...workerConfig(
|
|
83
|
+
...workerConfig(resourceName(options.name, 'workflows')),
|
|
66
84
|
workflows: [
|
|
67
85
|
{
|
|
68
|
-
name:
|
|
86
|
+
name: resourceName(options.name, 'workflow'),
|
|
69
87
|
binding: 'WORKFLOW',
|
|
70
88
|
class_name: 'ApplicationWorkflow',
|
|
71
89
|
},
|
|
@@ -74,11 +92,17 @@ export function scaffoldWorkflows({ root, options }) {
|
|
|
74
92
|
export class ApplicationWorkflow extends WorkflowEntrypoint<Env, { operationId: string }> {
|
|
75
93
|
async run(event: WorkflowEvent<{ operationId: string }>, step: WorkflowStep) {
|
|
76
94
|
return step.do('validate', { retries: { limit: 3, delay: '10 seconds', backoff: 'exponential' } }, async () => {
|
|
77
|
-
if (
|
|
95
|
+
if (typeof event.payload.operationId !== 'string' || !event.payload.operationId) {
|
|
96
|
+
throw new Error('operationId required');
|
|
97
|
+
}
|
|
78
98
|
return { operationId: event.payload.operationId };
|
|
79
99
|
});
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
|
-
export default {
|
|
102
|
+
export default {
|
|
103
|
+
async fetch() {
|
|
104
|
+
return new Response('Use an authorized server binding to create workflows', { status: 403 });
|
|
105
|
+
},
|
|
106
|
+
};
|
|
83
107
|
`);
|
|
84
108
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WORKER_DEV_PORTS } from '../dependencies.js';
|
|
2
|
+
import { put } from './files.js';
|
|
3
|
+
/** Human-readable companion to the current resolved registry, refreshed by add. */
|
|
4
|
+
export function writeStackDocumentation(root, registry) {
|
|
5
|
+
const sections = registry.items.map((item) => {
|
|
6
|
+
const lines = [
|
|
7
|
+
`## ${item.name}`,
|
|
8
|
+
'',
|
|
9
|
+
item.description + '.',
|
|
10
|
+
'',
|
|
11
|
+
`Targets: ${item.targets.map((target) => `\`${target}\``).join(', ')}.`,
|
|
12
|
+
];
|
|
13
|
+
if (item.environment.length)
|
|
14
|
+
lines.push(`Environment: ${item.environment.map((key) => `\`${key}\``).join(', ')}.`);
|
|
15
|
+
if (item.bindings)
|
|
16
|
+
lines.push('', 'Worker configuration:', '', '```json', JSON.stringify(item.bindings, null, 2), '```');
|
|
17
|
+
if (item.setup) {
|
|
18
|
+
for (const [label, entries] of [
|
|
19
|
+
['Required setup', item.setup.resources],
|
|
20
|
+
['Credentials', item.setup.credentials],
|
|
21
|
+
['Verify before delivery', item.setup.verify],
|
|
22
|
+
]) {
|
|
23
|
+
if (!entries.length)
|
|
24
|
+
continue;
|
|
25
|
+
lines.push('', `${label}:`, '', ...entries.map((entry) => `- ${entry}`));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
});
|
|
30
|
+
const ports = Object.entries(WORKER_DEV_PORTS)
|
|
31
|
+
.filter(([app]) => registry.items.some((item) => item.targets.includes(`apps/${app}`)))
|
|
32
|
+
.map(([app, port]) => `| ${app} | ${port.http} | ${port.inspector} |`);
|
|
33
|
+
put(root, 'docs/stack.md', [
|
|
34
|
+
'# Selected stack',
|
|
35
|
+
'',
|
|
36
|
+
`Generated from the bundled Anhedral ${registry.cliVersion} registry. This file is refreshed by add. Keep product decisions in docs/architecture.md and acceptance evidence in docs/verification.md.`,
|
|
37
|
+
'',
|
|
38
|
+
'These are scaffolds and integration boundaries. Provider access, migrations, authorization and live behavior require the setup and checks listed below.',
|
|
39
|
+
...(ports.length
|
|
40
|
+
? [
|
|
41
|
+
'',
|
|
42
|
+
'## Local Worker ports',
|
|
43
|
+
'',
|
|
44
|
+
'| Application | HTTP | Inspector |',
|
|
45
|
+
'| --- | --- | --- |',
|
|
46
|
+
...ports,
|
|
47
|
+
]
|
|
48
|
+
: []),
|
|
49
|
+
'',
|
|
50
|
+
...sections.flatMap((section) => [section, '']),
|
|
51
|
+
].join('\n'));
|
|
52
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { json, patchPackage, put, readJson } from './files.js';
|
|
2
|
+
/** Shared data and a Tailwind v3 preset for Expo/NativeWind and WXT. */
|
|
3
|
+
export function scaffoldStyling({ root }) {
|
|
4
|
+
put(root, 'packages/styling/src/tokens.cjs', `exports.light = {
|
|
5
|
+
'--background': '0 0% 100%',
|
|
6
|
+
'--foreground': '0 0% 9%',
|
|
7
|
+
'--card': '0 0% 100%',
|
|
8
|
+
'--card-foreground': '0 0% 9%',
|
|
9
|
+
'--popover': '0 0% 100%',
|
|
10
|
+
'--popover-foreground': '0 0% 9%',
|
|
11
|
+
'--primary': '0 0% 9%',
|
|
12
|
+
'--primary-foreground': '0 0% 98%',
|
|
13
|
+
'--secondary': '0 0% 96.1%',
|
|
14
|
+
'--secondary-foreground': '0 0% 9%',
|
|
15
|
+
'--muted': '0 0% 96.1%',
|
|
16
|
+
'--muted-foreground': '0 0% 45.1%',
|
|
17
|
+
'--accent': '0 0% 96.1%',
|
|
18
|
+
'--accent-foreground': '0 0% 9%',
|
|
19
|
+
'--destructive': '0 84.2% 60.2%',
|
|
20
|
+
'--destructive-foreground': '0 0% 98%',
|
|
21
|
+
'--border': '0 0% 89.8%',
|
|
22
|
+
'--input': '0 0% 89.8%',
|
|
23
|
+
'--ring': '0 0% 63.9%',
|
|
24
|
+
'--radius': '0.5rem',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
exports.dark = {
|
|
28
|
+
...exports.light,
|
|
29
|
+
'--background': '0 0% 9%',
|
|
30
|
+
'--foreground': '0 0% 98%',
|
|
31
|
+
'--card': '0 0% 9%',
|
|
32
|
+
'--card-foreground': '0 0% 98%',
|
|
33
|
+
'--popover': '0 0% 9%',
|
|
34
|
+
'--popover-foreground': '0 0% 98%',
|
|
35
|
+
'--primary': '0 0% 98%',
|
|
36
|
+
'--primary-foreground': '0 0% 9%',
|
|
37
|
+
'--secondary': '0 0% 14.9%',
|
|
38
|
+
'--secondary-foreground': '0 0% 98%',
|
|
39
|
+
'--muted': '0 0% 14.9%',
|
|
40
|
+
'--muted-foreground': '0 0% 63.9%',
|
|
41
|
+
'--accent': '0 0% 14.9%',
|
|
42
|
+
'--accent-foreground': '0 0% 98%',
|
|
43
|
+
'--destructive': '0 70.9% 59.4%',
|
|
44
|
+
'--border': '0 0% 14.9%',
|
|
45
|
+
'--input': '0 0% 14.9%',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
exports.spacing = { small: 4, medium: 8, large: 16 };
|
|
49
|
+
`);
|
|
50
|
+
put(root, 'packages/styling/tailwind.cjs', `const { light, dark } = require('./src/tokens.cjs');
|
|
51
|
+
const color = (name) => 'hsl(var(--' + name + '))';
|
|
52
|
+
const surface = (name) => ({ DEFAULT: color(name), foreground: color(name + '-foreground') });
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
darkMode: 'class',
|
|
56
|
+
theme: {
|
|
57
|
+
extend: {
|
|
58
|
+
colors: {
|
|
59
|
+
border: color('border'), input: color('input'), ring: color('ring'),
|
|
60
|
+
background: color('background'), foreground: color('foreground'),
|
|
61
|
+
primary: surface('primary'), secondary: surface('secondary'),
|
|
62
|
+
destructive: surface('destructive'), muted: surface('muted'),
|
|
63
|
+
accent: surface('accent'), popover: surface('popover'), card: surface('card'),
|
|
64
|
+
},
|
|
65
|
+
borderRadius: {
|
|
66
|
+
lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
plugins: [function themeVariables({ addBase }) {
|
|
71
|
+
addBase({ ':root': light, '.dark:root': dark });
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
`);
|
|
75
|
+
patchPackage(root, 'packages/styling/package.json', {
|
|
76
|
+
exports: { '.': './src/index.ts', './tailwind': './tailwind.cjs' },
|
|
77
|
+
});
|
|
78
|
+
const config = readJson(root, 'packages/styling/tsconfig.json');
|
|
79
|
+
json(root, 'packages/styling/tsconfig.json', {
|
|
80
|
+
...config,
|
|
81
|
+
compilerOptions: { ...config.compilerOptions, allowJs: true },
|
|
82
|
+
include: ['src/**/*.ts', 'src/**/*.cjs', 'tailwind.cjs'],
|
|
83
|
+
});
|
|
84
|
+
}
|