create-fate 1.0.4 → 1.3.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 (206) hide show
  1. package/README.md +3 -0
  2. package/bin/create-fate.mjs +594 -9
  3. package/package.json +2 -2
  4. package/templates/fate/README.md +3 -0
  5. package/templates/fate/_shared/vue-app/package.json +54 -0
  6. package/templates/fate/_shared/vue-app/pages/index.vue +57 -0
  7. package/templates/fate/_shared/vue-app/pages/layout.vue +32 -0
  8. package/templates/fate/_shared/vue-app/pages/post/[id].vue +24 -0
  9. package/templates/fate/_shared/vue-app/src/App.css +63 -0
  10. package/templates/fate/_shared/vue-app/src/fateViews.ts +14 -0
  11. package/templates/fate/_shared/vue-app/src/lib/cx.ts +6 -0
  12. package/templates/fate/_shared/vue-app/src/lib/env.ts +3 -0
  13. package/templates/fate/_shared/vue-app/src/ui/Button.vue +67 -0
  14. package/templates/fate/_shared/vue-app/src/ui/Card.vue +16 -0
  15. package/templates/fate/_shared/vue-app/src/ui/Error.vue +9 -0
  16. package/templates/fate/_shared/vue-app/src/ui/ErrorBoundary.vue +22 -0
  17. package/templates/fate/_shared/vue-app/src/ui/Header.vue +35 -0
  18. package/templates/fate/_shared/vue-app/src/ui/Link.vue +15 -0
  19. package/templates/fate/_shared/vue-app/src/ui/PostCard.vue +29 -0
  20. package/templates/fate/_shared/vue-app/src/ui/Section.vue +19 -0
  21. package/templates/fate/_shared/vue-app/src/ui/Thinking.vue +11 -0
  22. package/templates/fate/_shared/vue-app/src/ui/UserCard.vue +23 -0
  23. package/templates/fate/_shared/vue-app/src/vue-shims.d.ts +6 -0
  24. package/templates/fate/_shared/vue-app/tsconfig.json +22 -0
  25. package/templates/fate/_shared/vue-app/vite.config.ts +58 -0
  26. package/templates/fate/_shared/vue-app/void.json +5 -0
  27. package/templates/fate/cloudflare/.github/workflows/push.yml +27 -0
  28. package/templates/fate/cloudflare/.vite-hooks/pre-commit +1 -0
  29. package/templates/fate/cloudflare/.vscode/extensions.json +12 -0
  30. package/templates/fate/cloudflare/.vscode/settings.json +25 -0
  31. package/templates/fate/cloudflare/AGENTS.md +55 -0
  32. package/templates/fate/cloudflare/README.md +126 -0
  33. package/templates/fate/cloudflare/_gitignore +20 -0
  34. package/templates/fate/cloudflare/client/package.json +56 -0
  35. package/templates/fate/cloudflare/client/pages/index.tsx +96 -0
  36. package/templates/fate/cloudflare/client/pages/layout.tsx +82 -0
  37. package/templates/fate/cloudflare/client/pages/login.tsx +107 -0
  38. package/templates/fate/cloudflare/client/pages/post/[id].tsx +22 -0
  39. package/templates/fate/cloudflare/client/pages/search.tsx +99 -0
  40. package/templates/fate/cloudflare/client/public/icon.svg +1 -0
  41. package/templates/fate/cloudflare/client/src/App.css +153 -0
  42. package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +5 -0
  43. package/templates/fate/cloudflare/client/src/lib/cx.tsx +6 -0
  44. package/templates/fate/cloudflare/client/src/lib/env.tsx +5 -0
  45. package/templates/fate/cloudflare/client/src/ui/Badge.tsx +31 -0
  46. package/templates/fate/cloudflare/client/src/ui/Button.tsx +108 -0
  47. package/templates/fate/cloudflare/client/src/ui/Card.tsx +32 -0
  48. package/templates/fate/cloudflare/client/src/ui/CommentCard.tsx +75 -0
  49. package/templates/fate/cloudflare/client/src/ui/Error.tsx +19 -0
  50. package/templates/fate/cloudflare/client/src/ui/H2.tsx +15 -0
  51. package/templates/fate/cloudflare/client/src/ui/H3.tsx +20 -0
  52. package/templates/fate/cloudflare/client/src/ui/Header.tsx +98 -0
  53. package/templates/fate/cloudflare/client/src/ui/Input.tsx +17 -0
  54. package/templates/fate/cloudflare/client/src/ui/Link.tsx +11 -0
  55. package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +52 -0
  56. package/templates/fate/cloudflare/client/src/ui/PostCard.tsx +303 -0
  57. package/templates/fate/cloudflare/client/src/ui/Section.tsx +23 -0
  58. package/templates/fate/cloudflare/client/src/ui/UserCard.tsx +131 -0
  59. package/templates/fate/cloudflare/client/src/user/AuthClient.tsx +6 -0
  60. package/templates/fate/cloudflare/client/translations/de_DE.json +689 -0
  61. package/templates/fate/cloudflare/client/translations/ja_JP.json +689 -0
  62. package/templates/fate/cloudflare/client/vite.config.ts +45 -0
  63. package/templates/fate/cloudflare/client/void.json +29 -0
  64. package/templates/fate/cloudflare/package.json +46 -0
  65. package/templates/fate/cloudflare/pnpm-workspace.yaml +39 -0
  66. package/templates/fate/cloudflare/server/.env.example +1 -0
  67. package/templates/fate/cloudflare/server/db/db.ts +30 -0
  68. package/templates/fate/cloudflare/server/db/migrations/20260508073357_initial_schema.sql +133 -0
  69. package/templates/fate/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql +170 -0
  70. package/templates/fate/cloudflare/server/db/migrations/meta/20260508073357_snapshot.json +868 -0
  71. package/templates/fate/cloudflare/server/db/migrations/meta/_journal.json +20 -0
  72. package/templates/fate/cloudflare/server/db/queries.ts +211 -0
  73. package/templates/fate/cloudflare/server/db/schema.ts +332 -0
  74. package/templates/fate/cloudflare/server/package.json +34 -0
  75. package/templates/fate/cloudflare/server/src/fate/commentPermissions.ts +17 -0
  76. package/templates/fate/cloudflare/server/src/fate/context.ts +15 -0
  77. package/templates/fate/cloudflare/server/src/fate/live.ts +7 -0
  78. package/templates/fate/cloudflare/server/src/fate/server.ts +411 -0
  79. package/templates/fate/cloudflare/server/src/fate/views.ts +164 -0
  80. package/templates/fate/cloudflare/server/src/index.ts +80 -0
  81. package/templates/fate/cloudflare/server/src/lib/auth.ts +66 -0
  82. package/templates/fate/cloudflare/server/src/lib/origins.ts +17 -0
  83. package/templates/fate/cloudflare/server/src/router.ts +2 -0
  84. package/templates/fate/cloudflare/server/src/user/SessionUser.tsx +17 -0
  85. package/templates/fate/cloudflare/server/tsconfig.json +10 -0
  86. package/templates/fate/cloudflare/server/wrangler.jsonc +29 -0
  87. package/templates/fate/cloudflare/tsconfig.json +33 -0
  88. package/templates/fate/cloudflare/vite.config.ts +72 -0
  89. package/templates/fate/drizzle/README.md +2 -8
  90. package/templates/fate/drizzle/client/pages/layout.tsx +2 -2
  91. package/templates/fate/drizzle/client/src/ui/PostCard.tsx +38 -15
  92. package/templates/fate/drizzle/pnpm-workspace.yaml +9 -3
  93. package/templates/fate/drizzle/server/vite.config.ts +1 -1
  94. package/templates/fate/graphql/.github/workflows/push.yml +27 -0
  95. package/templates/fate/graphql/.vite-hooks/pre-commit +1 -0
  96. package/templates/fate/graphql/.vscode/extensions.json +12 -0
  97. package/templates/fate/graphql/.vscode/settings.json +25 -0
  98. package/templates/fate/graphql/AGENTS.md +55 -0
  99. package/templates/fate/graphql/README.md +121 -0
  100. package/templates/fate/graphql/_gitignore +23 -0
  101. package/templates/fate/graphql/client/package.json +57 -0
  102. package/templates/fate/graphql/client/pages/index.tsx +96 -0
  103. package/templates/fate/graphql/client/pages/layout.tsx +83 -0
  104. package/templates/fate/graphql/client/pages/login.tsx +107 -0
  105. package/templates/fate/graphql/client/pages/post/[id].tsx +22 -0
  106. package/templates/fate/graphql/client/pages/search.tsx +99 -0
  107. package/templates/fate/graphql/client/public/icon.svg +1 -0
  108. package/templates/fate/graphql/client/src/App.css +153 -0
  109. package/templates/fate/graphql/client/src/lib/AvailableLanguages.tsx +5 -0
  110. package/templates/fate/graphql/client/src/lib/cx.tsx +6 -0
  111. package/templates/fate/graphql/client/src/lib/env.tsx +5 -0
  112. package/templates/fate/graphql/client/src/ui/Badge.tsx +31 -0
  113. package/templates/fate/graphql/client/src/ui/Button.tsx +108 -0
  114. package/templates/fate/graphql/client/src/ui/Card.tsx +32 -0
  115. package/templates/fate/graphql/client/src/ui/CommentCard.tsx +75 -0
  116. package/templates/fate/graphql/client/src/ui/Error.tsx +19 -0
  117. package/templates/fate/graphql/client/src/ui/H2.tsx +15 -0
  118. package/templates/fate/graphql/client/src/ui/H3.tsx +20 -0
  119. package/templates/fate/graphql/client/src/ui/Header.tsx +98 -0
  120. package/templates/fate/graphql/client/src/ui/Input.tsx +17 -0
  121. package/templates/fate/graphql/client/src/ui/Link.tsx +11 -0
  122. package/templates/fate/graphql/client/src/ui/LocaleSwitcher.tsx +52 -0
  123. package/templates/fate/graphql/client/src/ui/PostCard.tsx +303 -0
  124. package/templates/fate/graphql/client/src/ui/Section.tsx +23 -0
  125. package/templates/fate/graphql/client/src/ui/UserCard.tsx +131 -0
  126. package/templates/fate/graphql/client/src/user/AuthClient.tsx +6 -0
  127. package/templates/fate/graphql/client/translations/de_DE.json +689 -0
  128. package/templates/fate/graphql/client/translations/ja_JP.json +689 -0
  129. package/templates/fate/graphql/client/vite.config.ts +40 -0
  130. package/templates/fate/graphql/client/void.json +29 -0
  131. package/templates/fate/graphql/docker-compose.yml +11 -0
  132. package/templates/fate/graphql/package.json +47 -0
  133. package/templates/fate/graphql/pnpm-workspace.yaml +45 -0
  134. package/templates/fate/graphql/server/.env.example +5 -0
  135. package/templates/fate/graphql/server/README.md +27 -0
  136. package/templates/fate/graphql/server/package.json +64 -0
  137. package/templates/fate/graphql/server/prisma.config.ts +17 -0
  138. package/templates/fate/graphql/server/scripts/generate-graphql.tsx +40 -0
  139. package/templates/fate/graphql/server/src/__tests__/index.test.tsx +5 -0
  140. package/templates/fate/graphql/server/src/app.tsx +47 -0
  141. package/templates/fate/graphql/server/src/graphql/builder.tsx +121 -0
  142. package/templates/fate/graphql/server/src/graphql/context.tsx +22 -0
  143. package/templates/fate/graphql/server/src/graphql/fate.ts +85 -0
  144. package/templates/fate/graphql/server/src/graphql/lib/authDirectives.tsx +93 -0
  145. package/templates/fate/graphql/server/src/graphql/lib/decodeGlobalID.tsx +12 -0
  146. package/templates/fate/graphql/server/src/graphql/lib/decodeIDOrThrow.tsx +10 -0
  147. package/templates/fate/graphql/server/src/graphql/lib/encodeGlobalID.tsx +5 -0
  148. package/templates/fate/graphql/server/src/graphql/live.tsx +241 -0
  149. package/templates/fate/graphql/server/src/graphql/mutations.tsx +265 -0
  150. package/templates/fate/graphql/server/src/graphql/nodes/Category.tsx +35 -0
  151. package/templates/fate/graphql/server/src/graphql/nodes/Comment.tsx +46 -0
  152. package/templates/fate/graphql/server/src/graphql/nodes/Event.tsx +71 -0
  153. package/templates/fate/graphql/server/src/graphql/nodes/Post.tsx +45 -0
  154. package/templates/fate/graphql/server/src/graphql/nodes/Tag.tsx +11 -0
  155. package/templates/fate/graphql/server/src/graphql/nodes/User.tsx +28 -0
  156. package/templates/fate/graphql/server/src/graphql/schema.tsx +16 -0
  157. package/templates/fate/graphql/server/src/graphql/views.ts +193 -0
  158. package/templates/fate/graphql/server/src/index.tsx +45 -0
  159. package/templates/fate/graphql/server/src/lib/auth.tsx +31 -0
  160. package/templates/fate/graphql/server/src/lib/env.tsx +3 -0
  161. package/templates/fate/graphql/server/src/lib/origins.ts +18 -0
  162. package/templates/fate/graphql/server/src/prisma/prisma.tsx +9 -0
  163. package/templates/fate/graphql/server/src/prisma/schema.prisma +181 -0
  164. package/templates/fate/graphql/server/src/prisma/seed.tsx +161 -0
  165. package/templates/fate/graphql/server/src/user/SessionUser.tsx +17 -0
  166. package/templates/fate/graphql/server/src/user/isAdmin.tsx +3 -0
  167. package/templates/fate/graphql/server/tsconfig.json +33 -0
  168. package/templates/fate/graphql/server/vite.config.ts +55 -0
  169. package/templates/fate/graphql/tsconfig.json +33 -0
  170. package/templates/fate/graphql/vite.config.ts +80 -0
  171. package/templates/fate/graphql-client/.env.example +3 -0
  172. package/templates/fate/graphql-client/AGENTS.md +3 -0
  173. package/templates/fate/graphql-client/README.md +69 -0
  174. package/templates/fate/graphql-client/_gitignore +14 -0
  175. package/templates/fate/graphql-client/package.json +62 -0
  176. package/templates/fate/graphql-client/pages/index.tsx +69 -0
  177. package/templates/fate/graphql-client/pages/layout.tsx +41 -0
  178. package/templates/fate/graphql-client/pnpm-workspace.yaml +38 -0
  179. package/templates/fate/graphql-client/public/icon.svg +8 -0
  180. package/templates/fate/graphql-client/src/App.css +28 -0
  181. package/templates/fate/graphql-client/src/fate/graphql.ts +52 -0
  182. package/templates/fate/graphql-client/tsconfig.json +30 -0
  183. package/templates/fate/graphql-client/vite.config.ts +87 -0
  184. package/templates/fate/graphql-client/void.json +29 -0
  185. package/templates/fate/http/README.md +2 -8
  186. package/templates/fate/http/client/pages/layout.tsx +2 -2
  187. package/templates/fate/http/client/src/ui/PostCard.tsx +37 -15
  188. package/templates/fate/http/pnpm-workspace.yaml +9 -3
  189. package/templates/fate/http/server/vite.config.ts +1 -1
  190. package/templates/fate/prisma/README.md +2 -8
  191. package/templates/fate/prisma/client/pages/layout.tsx +2 -2
  192. package/templates/fate/prisma/client/src/ui/PostCard.tsx +37 -15
  193. package/templates/fate/prisma/pnpm-workspace.yaml +9 -3
  194. package/templates/fate/prisma/server/vite.config.ts +1 -1
  195. package/templates/fate/prisma/vite.config.ts +1 -1
  196. package/templates/fate/void/package.json +10 -2
  197. package/templates/fate/void/pnpm-workspace.yaml +12 -4
  198. package/templates/fate/void/src/ui/CategoryCard.tsx +3 -7
  199. package/templates/fate/void/src/ui/PostCard.tsx +38 -15
  200. package/templates/fate/void/vite.config.ts +5 -0
  201. package/templates/fate/drizzle/public/fate-logo-dark.svg +0 -4
  202. package/templates/fate/drizzle/public/fate-logo.svg +0 -4
  203. package/templates/fate/http/public/fate-logo-dark.svg +0 -4
  204. package/templates/fate/http/public/fate-logo.svg +0 -4
  205. package/templates/fate/prisma/public/fate-logo-dark.svg +0 -4
  206. package/templates/fate/prisma/public/fate-logo.svg +0 -4
@@ -7,11 +7,26 @@ import { fileURLToPath, URL } from 'node:url';
7
7
  import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts';
8
8
 
9
9
  const variants = {
10
+ cloudflare: {
11
+ description: 'Cloudflare Workers with D1 and Drizzle',
12
+ label: 'Cloudflare',
13
+ template: 'cloudflare',
14
+ },
10
15
  drizzle: {
11
16
  description: 'tRPC with Drizzle',
12
17
  label: 'Drizzle',
13
18
  template: 'drizzle',
14
19
  },
20
+ graphql: {
21
+ description: 'GraphQL with Prisma',
22
+ label: 'GraphQL',
23
+ template: 'graphql',
24
+ },
25
+ 'graphql-client': {
26
+ description: 'Existing GraphQL server',
27
+ label: 'GraphQL Client',
28
+ template: 'graphql-client',
29
+ },
15
30
  http: {
16
31
  description: 'Native HTTP with Drizzle',
17
32
  label: 'Native HTTP',
@@ -29,14 +44,27 @@ const variants = {
29
44
  },
30
45
  };
31
46
 
32
- const fateDependencyNames = ['@nkzw/fate', 'react-fate', 'void-fate'];
47
+ const frontendFrameworks = {
48
+ react: {
49
+ description: 'React with react-fate',
50
+ label: 'React',
51
+ },
52
+ vue: {
53
+ description: 'Vue with vue-fate',
54
+ label: 'Vue',
55
+ },
56
+ };
57
+
58
+ const fateDependencyNames = ['@nkzw/fate', 'cf-fate', 'react-fate', 'void-fate', 'vue-fate'];
33
59
 
34
60
  const usage = () => {
35
- process.stdout.write(`Usage: create-fate [directory] [--template void|drizzle|http|prisma]
61
+ process.stdout
62
+ .write(`Usage: create-fate [directory] [--template cloudflare|void|drizzle|graphql|graphql-client|http|prisma] [--framework react|vue]
36
63
 
37
64
  Create a new fate app.
38
65
 
39
66
  Options:
67
+ --framework, -f UI framework to create (react or vue)
40
68
  --template, -t Template variant to create
41
69
  --no-setup Skip dependency installation and fate client generation
42
70
  --help, -h Show this help message
@@ -51,6 +79,7 @@ const normalizePackageName = (name) =>
51
79
  .replaceAll(/^-+|-+$/g, '') || 'my-app';
52
80
 
53
81
  const parseArgs = (args) => {
82
+ let framework;
54
83
  let setup = true;
55
84
  let targetDir;
56
85
  let variant;
@@ -66,6 +95,21 @@ const parseArgs = (args) => {
66
95
  continue;
67
96
  }
68
97
 
98
+ if (arg === '--framework' || arg === '-f' || arg === '--ui') {
99
+ framework = args[++index];
100
+ continue;
101
+ }
102
+
103
+ if (arg?.startsWith('--framework=')) {
104
+ framework = arg.slice('--framework='.length);
105
+ continue;
106
+ }
107
+
108
+ if (arg?.startsWith('--ui=')) {
109
+ framework = arg.slice('--ui='.length);
110
+ continue;
111
+ }
112
+
69
113
  if (arg === '--template' || arg === '-t' || arg === '--variant') {
70
114
  variant = args[++index];
71
115
  continue;
@@ -89,7 +133,7 @@ const parseArgs = (args) => {
89
133
  throw new Error(`Unexpected argument: ${arg}`);
90
134
  }
91
135
 
92
- return { setup, targetDir, variant };
136
+ return { framework, setup, targetDir, variant };
93
137
  };
94
138
 
95
139
  const validateTargetDir = (value) => {
@@ -155,6 +199,24 @@ const promptForVariant = async () => {
155
199
  return result;
156
200
  };
157
201
 
202
+ const promptForFramework = async () => {
203
+ const result = await select({
204
+ initialValue: 'react',
205
+ message: 'Select a UI framework',
206
+ options: Object.entries(frontendFrameworks).map(([value, framework]) => ({
207
+ hint: framework.description,
208
+ label: framework.label,
209
+ value,
210
+ })),
211
+ });
212
+
213
+ if (isCancel(result)) {
214
+ abortPrompt();
215
+ }
216
+
217
+ return result;
218
+ };
219
+
158
220
  const getRegistryURL = () => {
159
221
  const registry = process.env.npm_config_registry || 'https://registry.npmjs.org/';
160
222
  return registry.endsWith('/') ? registry : `${registry}/`;
@@ -215,10 +277,21 @@ const resolveFateDependencyVersions = async (rootDir) => {
215
277
  const dependencies = [...collectFateDependencies(rootDir)];
216
278
  return Object.fromEntries(
217
279
  await Promise.all(
218
- dependencies.map(async (dependencyName) => [
219
- dependencyName,
220
- `^${await fetchLatestPackageVersion(dependencyName)}`,
221
- ]),
280
+ dependencies.map(async (dependencyName) => {
281
+ if (dependencyName === 'cf-fate') {
282
+ return [dependencyName, 'latest'];
283
+ }
284
+
285
+ try {
286
+ return [dependencyName, `^${await fetchLatestPackageVersion(dependencyName)}`];
287
+ } catch (error) {
288
+ if (dependencyName === 'vue-fate') {
289
+ return [dependencyName, 'latest'];
290
+ }
291
+
292
+ throw error;
293
+ }
294
+ }),
222
295
  ),
223
296
  );
224
297
  };
@@ -261,6 +334,77 @@ const updatePackageJsonFiles = (rootDir, dir, packageName, fateDependencyVersion
261
334
  }
262
335
  };
263
336
 
337
+ const readPackageJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'));
338
+
339
+ const writePackageJson = (filePath, json) => {
340
+ fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`);
341
+ };
342
+
343
+ const removePackageEntries = (record, names) => {
344
+ if (!record) {
345
+ return;
346
+ }
347
+
348
+ for (const name of names) {
349
+ delete record[name];
350
+ }
351
+ };
352
+
353
+ const reactPackageEntries = [
354
+ '@nkzw/babel-preset-fbtee',
355
+ '@radix-ui/react-slot',
356
+ '@nkzw/stack',
357
+ '@rolldown/plugin-babel',
358
+ '@types/react',
359
+ '@types/react-dom',
360
+ '@vitejs/plugin-react',
361
+ '@void/react',
362
+ 'babel-plugin-react-compiler',
363
+ 'eslint-plugin-react-hooks',
364
+ 'fbtee',
365
+ 'lucide-react',
366
+ 'react',
367
+ 'react-dom',
368
+ 'react-error-boundary',
369
+ 'react-fate',
370
+ ];
371
+
372
+ const vueAgentsGuide = `# Using _fate_ for Vue applications
373
+
374
+ This guidance is for agents working in projects bootstrapped from the fate Vue template. It focuses on Vue-native composables for fate's view composition and request execution.
375
+
376
+ ## Core Workflow
377
+
378
+ - **Define views with explicit selections:** Define reusable view objects with \`view<T>()({...})\`, compose them via spreads, and resolve them with \`useView\`.
379
+ - **Resolve data via references:** Avoid passing raw fetched objects; instead pass \`ViewRef\` values to downstream components to keep selections scoped and cache-aware. Then call \`const result = useView(viewDefinition, ref)\` in the component that reads the data.
380
+ - **Request data at route roots:** Use \`useRequest\` to compose all views required for a route into a single typed request; use Vue \`<Suspense>\` and an error boundary around route content.
381
+ - **Compose reusable views:** Extract nested selections into their own views, such as \`UserView\`, and compose them inside parent views, such as \`author: UserView\`, to avoid duplication and overfetching.
382
+
383
+ ## Mutations
384
+
385
+ - **Use generated mutations deliberately:** Trigger mutations through the generated fate client APIs and pass optimistic payloads when the UI should update immediately.
386
+ - **Select extra fields when side effects matter:** When a mutation affects related entities, pass a \`view\` to fetch the needed fields in the same round-trip and keep the cache coherent for all dependent views.
387
+ - **Handle loading and errors explicitly:** Vue components should expose pending and error states through refs/resources rather than React Action patterns.
388
+ - **Handle deletions and resets explicitly:** Use the \`delete: true\` flag to evict records from the cache, and send the \`'reset'\` token to clear stale mutation errors without re-running the mutation.
389
+
390
+ ## Common Pitfalls
391
+
392
+ - **Use ViewRefs:** Pass references instead of raw data. Keep components focused on views and \`ViewRef\` values to let fate manage data masking and updates.
393
+ - **Co-locate data needs:** Keep view definitions near their components and compose them upward to the request root for predictable fetching.
394
+ - **Use Vue Suspense:** Wrap pages in \`<Suspense>\` and an error boundary instead of ad-hoc loading/error branches where route-level data is required.
395
+ - **Do not duplicate types:** Keep generated fate types and views imported from the server package. Avoid redefining entity shapes on the client.
396
+ - **\`useRequest\` only at route roots:** Create a dedicated \`useRequest\` call per screen root that pulls together all child views; do not scatter \`useRequest\` across leaf components unless it is necessary to issue a separate request.
397
+ - **fate client support:** fate's local client support files are maintained by the Vite/fate tooling. Do not manually edit \`.fate\` files; make the proper schema changes on the server and run \`pnpm fate:generate\` when working outside Vite dev.
398
+
399
+ Full documentation can be found on the filesystem at \`./client/node_modules/vue-fate/README.md\` or online at [fate.technology](https://fate.technology/).
400
+
401
+ ## Review Checklist for Agents
402
+
403
+ - [ ] Every component that reads server data does so through \`useView\` and receives a \`ViewRef\` prop.
404
+ - [ ] Route components gather data with a single \`useRequest\` call.
405
+ - [ ] Mutations use generated fate client APIs with optimistic updates and necessary \`view\` selections.
406
+ - [ ] Shared selections live in reusable views using regular JavaScript object spreads rather than duplicated field lists.`;
407
+
264
408
  const restoreTemplateFileNames = (dir) => {
265
409
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
266
410
  const entryPath = path.join(dir, entry.name);
@@ -275,6 +419,425 @@ const restoreTemplateFileNames = (dir) => {
275
419
  }
276
420
  };
277
421
 
422
+ const replaceInFiles = (dir, replacements) => {
423
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
424
+ if (entry.name === 'node_modules' || entry.name === '.git') {
425
+ continue;
426
+ }
427
+
428
+ const entryPath = path.join(dir, entry.name);
429
+ if (entry.isDirectory()) {
430
+ replaceInFiles(entryPath, replacements);
431
+ continue;
432
+ }
433
+
434
+ let source = fs.readFileSync(entryPath, 'utf8');
435
+ for (const [token, value] of Object.entries(replacements)) {
436
+ source = source.replaceAll(token, value);
437
+ }
438
+ fs.writeFileSync(entryPath, source);
439
+ }
440
+ };
441
+
442
+ const vueTransportConfigs = {
443
+ cloudflare: {
444
+ clientImports: `import env from '../src/lib/env.ts';`,
445
+ createOptions: `{
446
+ fetch: (input, init) =>
447
+ fetch(input, {
448
+ ...init,
449
+ credentials: 'include',
450
+ }),
451
+ liveUrl: \`\${env('SERVER_URL')}/fate-live\`,
452
+ url: \`\${env('SERVER_URL')}/fate\`,
453
+ }`,
454
+ dotenvConfig: `dotenv.config({
455
+ path: join(root, '../server', process.env.NODE_ENV === 'development' || process.env.DEV ? '.env' : '.prod.env'),
456
+ quiet: true,
457
+ })`,
458
+ envKeys: `['SERVER_URL']`,
459
+ envValues: `{
460
+ SERVER_URL: import.meta.env.VITE_SERVER_URL,
461
+ }`,
462
+ fateModule: '@app/server/src/router.ts',
463
+ fateTransport: `transport: 'cloudflare',`,
464
+ typeModule: '@app/server/src/router.ts',
465
+ },
466
+ drizzle: {
467
+ clientImports: `import { httpBatchLink } from '@trpc/client';
468
+ import env from '../src/lib/env.ts';`,
469
+ createOptions: `{
470
+ fetch: (input, init) =>
471
+ fetch(input, {
472
+ ...init,
473
+ credentials: 'include',
474
+ }),
475
+ links: [
476
+ httpBatchLink({
477
+ fetch: (input, init) =>
478
+ fetch(input, {
479
+ ...init,
480
+ credentials: 'include',
481
+ }),
482
+ url: \`\${env('SERVER_URL')}/trpc\`,
483
+ }),
484
+ ],
485
+ liveUrl: \`\${env('SERVER_URL')}/fate\`,
486
+ }`,
487
+ dotenvConfig: `dotenv.config({
488
+ path: join(root, '../server', process.env.NODE_ENV === 'development' || process.env.DEV ? '.env' : '.prod.env'),
489
+ quiet: true,
490
+ })`,
491
+ envKeys: `['SERVER_URL']`,
492
+ envValues: `{
493
+ SERVER_URL: import.meta.env.VITE_SERVER_URL,
494
+ }`,
495
+ fateModule: '@app/server/src/router.ts',
496
+ fateTransport: `transport: 'trpc',`,
497
+ typeModule: '@app/server/src/router.ts',
498
+ },
499
+ graphql: {
500
+ clientImports: `import env from '../src/lib/env.ts';`,
501
+ createOptions: `{
502
+ live: { url: \`\${env('SERVER_URL')}/graphql/stream\` },
503
+ url: \`\${env('SERVER_URL')}/graphql\`,
504
+ }`,
505
+ dotenvConfig: `dotenv.config({
506
+ path: join(root, '../server', process.env.NODE_ENV === 'development' || process.env.DEV ? '.env' : '.prod.env'),
507
+ quiet: true,
508
+ })`,
509
+ envKeys: `['SERVER_URL']`,
510
+ envValues: `{
511
+ SERVER_URL: import.meta.env.VITE_SERVER_URL,
512
+ }`,
513
+ fateModule: '@app/server/src/graphql/fate.ts',
514
+ fateTransport: `transport: 'graphql',`,
515
+ typeModule: '@app/server/src/graphql/fate.ts',
516
+ },
517
+ 'graphql-client': {
518
+ clientImports: ``,
519
+ createOptions: `{
520
+ live: import.meta.env.VITE_GRAPHQL_LIVE_URL
521
+ ? { url: import.meta.env.VITE_GRAPHQL_LIVE_URL }
522
+ : false,
523
+ url: import.meta.env.VITE_GRAPHQL_URL,
524
+ }`,
525
+ dotenvConfig: `dotenv.config({ path: join(root, '.env'), quiet: true })`,
526
+ envKeys: `[]`,
527
+ envValues: `{}`,
528
+ fateModule: './src/fate/graphql.ts',
529
+ fateTransport: `transport: 'graphql',`,
530
+ typeModule: './fate/graphql.ts',
531
+ },
532
+ http: {
533
+ clientImports: `import env from '../src/lib/env.ts';`,
534
+ createOptions: `{
535
+ fetch: (input, init) =>
536
+ fetch(input, {
537
+ ...init,
538
+ credentials: 'include',
539
+ }),
540
+ liveUrl: \`\${env('SERVER_URL')}/fate\`,
541
+ url: \`\${env('SERVER_URL')}/fate\`,
542
+ }`,
543
+ dotenvConfig: `dotenv.config({
544
+ path: join(root, '../server', process.env.NODE_ENV === 'development' || process.env.DEV ? '.env' : '.prod.env'),
545
+ quiet: true,
546
+ })`,
547
+ envKeys: `['SERVER_URL']`,
548
+ envValues: `{
549
+ SERVER_URL: import.meta.env.VITE_SERVER_URL,
550
+ }`,
551
+ fateModule: '@app/server/src/router.ts',
552
+ fateTransport: `transport: 'native',`,
553
+ typeModule: '@app/server/src/router.ts',
554
+ },
555
+ prisma: null,
556
+ void: {
557
+ clientImports: `import { useShared } from '@void/vue';
558
+ import type { SharedData } from '../src/lib/shared.ts';
559
+
560
+ const shared = useShared<SharedData>();`,
561
+ createOptions: `{
562
+ origin: shared.origin,
563
+ userId: shared.auth.user?.id,
564
+ }`,
565
+ dotenvConfig: `dotenv.config({ path: join(root, '.env'), quiet: true })`,
566
+ envKeys: `[]`,
567
+ envValues: `{}`,
568
+ fateModule: './src/fate/server.ts',
569
+ fateTransport: `transport: 'void',`,
570
+ typeModule: './fate/server.ts',
571
+ },
572
+ };
573
+ vueTransportConfigs.prisma = vueTransportConfigs.drizzle;
574
+
575
+ const vueTemplatePackage = () =>
576
+ path.resolve(packageRoot, 'templates', 'fate', '_shared', 'vue-app');
577
+
578
+ const frontendRootForVariant = (targetPath, selectedVariant) =>
579
+ selectedVariant === 'void' || selectedVariant === 'graphql-client'
580
+ ? targetPath
581
+ : path.join(targetPath, 'client');
582
+
583
+ const removeIfExists = (entryPath) => {
584
+ if (fs.existsSync(entryPath)) {
585
+ fs.rmSync(entryPath, { force: true, recursive: true });
586
+ }
587
+ };
588
+
589
+ const removeReactFrontend = (frontendRoot, selectedVariant) => {
590
+ if (selectedVariant === 'void' || selectedVariant === 'graphql-client') {
591
+ for (const entry of ['pages', 'src/ui']) {
592
+ removeIfExists(path.join(frontendRoot, entry));
593
+ }
594
+ removeIfExists(path.join(frontendRoot, 'src', 'App.css'));
595
+ removeIfExists(path.join(frontendRoot, 'src', 'lib', 'cx.tsx'));
596
+ removeIfExists(path.join(frontendRoot, 'src', 'user', 'AuthClient.tsx'));
597
+ return;
598
+ }
599
+
600
+ removeIfExists(frontendRoot);
601
+ };
602
+
603
+ const configureVuePackageJson = (frontendRoot, selectedVariant, originalPackageJson) => {
604
+ const packagePath = path.join(frontendRoot, 'package.json');
605
+ const vuePackageJson = readPackageJson(packagePath);
606
+ const packageJson = originalPackageJson
607
+ ? {
608
+ ...originalPackageJson,
609
+ dependencies: {
610
+ ...originalPackageJson.dependencies,
611
+ ...vuePackageJson.dependencies,
612
+ },
613
+ devDependencies: {
614
+ ...originalPackageJson.devDependencies,
615
+ ...vuePackageJson.devDependencies,
616
+ },
617
+ }
618
+ : vuePackageJson;
619
+
620
+ removePackageEntries(packageJson.dependencies, reactPackageEntries);
621
+ removePackageEntries(packageJson.devDependencies, reactPackageEntries);
622
+
623
+ if (selectedVariant !== 'drizzle' && selectedVariant !== 'prisma') {
624
+ delete packageJson.dependencies?.['@trpc/client'];
625
+ }
626
+
627
+ if (selectedVariant !== 'graphql' && selectedVariant !== 'graphql-client') {
628
+ delete packageJson.dependencies?.['graphql-sse'];
629
+ }
630
+
631
+ if (selectedVariant !== 'graphql-client' && selectedVariant !== 'void') {
632
+ packageJson.dependencies = {
633
+ '@app/server': 'workspace:*',
634
+ ...packageJson.dependencies,
635
+ };
636
+ }
637
+
638
+ if (selectedVariant === 'cloudflare') {
639
+ packageJson.dependencies = {
640
+ ...packageJson.dependencies,
641
+ 'cf-fate': 'latest',
642
+ };
643
+ }
644
+
645
+ if (packageJson.description) {
646
+ packageJson.description = packageJson.description.replaceAll('React', 'Vue');
647
+ }
648
+
649
+ writePackageJson(packagePath, packageJson);
650
+ };
651
+
652
+ const configureVueRootPackageJson = (targetPath) => {
653
+ const packagePath = path.join(targetPath, 'package.json');
654
+ if (!fs.existsSync(packagePath)) {
655
+ return;
656
+ }
657
+
658
+ const packageJson = readPackageJson(packagePath);
659
+ removePackageEntries(packageJson.dependencies, reactPackageEntries);
660
+ removePackageEntries(packageJson.devDependencies, reactPackageEntries);
661
+ removePackageEntries(packageJson.peerDependencies, reactPackageEntries);
662
+
663
+ if (packageJson.description) {
664
+ packageJson.description = packageJson.description.replaceAll('React', 'Vue');
665
+ }
666
+
667
+ writePackageJson(packagePath, packageJson);
668
+ };
669
+
670
+ const configureVueVoidServerFiles = (frontendRoot) => {
671
+ const oldSessionUser = path.join(frontendRoot, 'src', 'user', 'SessionUser.tsx');
672
+ const nextSessionUser = path.join(frontendRoot, 'src', 'user', 'SessionUser.ts');
673
+ if (fs.existsSync(oldSessionUser)) {
674
+ fs.renameSync(oldSessionUser, nextSessionUser);
675
+ }
676
+
677
+ const contextPath = path.join(frontendRoot, 'src', 'fate', 'context.ts');
678
+ fs.writeFileSync(
679
+ contextPath,
680
+ fs
681
+ .readFileSync(contextPath, 'utf8')
682
+ .replace('../user/SessionUser.tsx', '../user/SessionUser.ts'),
683
+ );
684
+ };
685
+
686
+ const configureVueAgents = (targetPath) => {
687
+ const agentsPath = path.join(targetPath, 'AGENTS.md');
688
+ if (!fs.existsSync(agentsPath)) {
689
+ fs.writeFileSync(agentsPath, `${vueAgentsGuide}\n`);
690
+ return;
691
+ }
692
+
693
+ const agents = fs.readFileSync(agentsPath, 'utf8');
694
+ const vitePlusIndex = agents.indexOf('<!--VITE PLUS START-->');
695
+ const suffix = vitePlusIndex === -1 ? agents : agents.slice(vitePlusIndex);
696
+ fs.writeFileSync(agentsPath, `${vueAgentsGuide}\n\n${suffix.trim()}\n`);
697
+ };
698
+
699
+ const configureVueReadme = (targetPath, selectedVariant) => {
700
+ const readmePath = path.join(targetPath, 'README.md');
701
+ if (!fs.existsSync(readmePath)) {
702
+ return;
703
+ }
704
+
705
+ const readme = fs.readFileSync(readmePath, 'utf8');
706
+ fs.writeFileSync(
707
+ readmePath,
708
+ readme
709
+ .replaceAll(
710
+ `vp create fate my-app --template ${selectedVariant}`,
711
+ `vp create fate my-app --template ${selectedVariant} --framework vue`,
712
+ )
713
+ .replaceAll('modern data client for React', 'modern data client for Vue')
714
+ .replaceAll(
715
+ 'data masking, Async React features, and',
716
+ 'data masking, Vue-native composables, and',
717
+ )
718
+ .replaceAll('Async React features', 'Vue-native composables')
719
+ .replaceAll('React applications', 'Vue applications')
720
+ .replaceAll(
721
+ '- [React](https://reactjs.org/) with [React Compiler](https://react.dev/learn/react-compiler) enabled',
722
+ '- [Vue](https://vuejs.org/)',
723
+ )
724
+ .replaceAll('The React client application', 'The Vue client application')
725
+ .replaceAll('React components', 'Vue components')
726
+ .replaceAll('React client', 'Vue client')
727
+ .replaceAll('`react-fate`', '`vue-fate`')
728
+ .replaceAll('`@void/react`', '`@void/vue`')
729
+ .replaceAll(', and React Compiler', '')
730
+ .replaceAll(' and React Compiler', '')
731
+ .replaceAll(
732
+ 'This template uses the Void pages router with Drizzle, `void-fate`, `vue-fate`, Better Auth, Tailwind.',
733
+ 'This template uses Vue, the Void pages router with Drizzle, `void-fate`, `vue-fate`, Better Auth, and Tailwind.',
734
+ ),
735
+ );
736
+ };
737
+
738
+ const configureVueVoidDemoContent = (targetPath) => {
739
+ replaceInFiles(targetPath, {
740
+ 'Actions in fate are exposed for useActionState, and mutations can carry an optimistic object alongside the input. The cache applies the optimistic update immediately, re-renders affected views, and rolls the affected records back if the server rejects the mutation. This example uses the like button because it is easy to see, but the same model works for comments and other records.':
741
+ 'Mutations in fate can carry an optimistic object alongside the input. The cache applies the optimistic update immediately, re-renders affected views, and rolls the affected records back if the server rejects the mutation. This example uses the like button because it is easy to see, but the same model works for comments and other records.',
742
+ 'async-react': 'vue-runtime',
743
+ 'Async React, Suspense, Actions, and the react-fate hooks.':
744
+ 'Vue composables, Suspense, resources, and the vue-fate APIs.',
745
+ 'normal React': 'the host framework',
746
+ 'react-fate': 'vue-fate',
747
+ 'React Integration': 'Vue Integration',
748
+ 'Suspense, Actions, use, and concurrent rendering patterns.':
749
+ 'Vue Suspense, composables, resources, and reactive rendering patterns.',
750
+ 'The cache lifetime post is a good reminder to retain manual requests outside React.':
751
+ 'The cache lifetime post is a good reminder to retain manual requests outside component setup.',
752
+ 'The React integration leans on Suspense instead of local loading flags.':
753
+ 'The Vue integration uses Suspense and composables instead of local loading flags.',
754
+ 'The React integration posts make it clear that fate is not trying to replace React state.':
755
+ 'The Vue integration posts make it clear that fate is not trying to replace Vue state.',
756
+ 'The server adapters now cover Prisma and Drizzle with the same data view concepts. Sources describe how to resolve records, fields, lists, counts, and computed values, while fate builds the selected shape for the client. This makes the examples useful for teams with different database layers without changing the React component model.':
757
+ 'The server adapters now cover Prisma and Drizzle with the same data view concepts. Sources describe how to resolve records, fields, lists, counts, and computed values, while fate builds the selected shape for the client. This makes the examples useful for teams with different database layers without changing the component data model.',
758
+ 'The useActionState integration is practical because the form code still looks like React instead of a custom mutation framework.':
759
+ 'The Vue composable integration is practical because the component code still looks like Vue instead of a custom mutation framework.',
760
+ 'The Void example is a nice proof that the transport and React APIs are portable.':
761
+ 'The Void example is a nice proof that the transport and framework adapters are portable.',
762
+ 'The Void example uses the same fate ideas in a full-stack app with file-system routing, shared data, auth, mutations, live comments, categories, tags, and events. It exists to prove the core protocol and React APIs are not tied to one server framework. The seed data includes this post so search and category views can show the newest example alongside the Prisma and Drizzle servers.':
763
+ 'The Void example uses the same fate ideas in a full-stack app with file-system routing, shared data, auth, mutations, live comments, categories, tags, and events. It exists to prove the core protocol and framework adapters are not tied to one server framework. The seed data includes this post so search and category views can show the newest example alongside the Prisma and Drizzle servers.',
764
+ });
765
+ };
766
+
767
+ const configureVueTemplate = (targetPath, selectedVariant) => {
768
+ const frontendRoot = frontendRootForVariant(targetPath, selectedVariant);
769
+ const originalPackageJson =
770
+ selectedVariant === 'void' || selectedVariant === 'graphql-client'
771
+ ? readPackageJson(path.join(frontendRoot, 'package.json'))
772
+ : null;
773
+
774
+ removeReactFrontend(frontendRoot, selectedVariant);
775
+ fs.mkdirSync(frontendRoot, { recursive: true });
776
+ fs.cpSync(vueTemplatePackage(), frontendRoot, { recursive: true });
777
+
778
+ if (selectedVariant === 'void') {
779
+ configureVueVoidServerFiles(frontendRoot);
780
+ }
781
+
782
+ configureVuePackageJson(frontendRoot, selectedVariant, originalPackageJson);
783
+ if (frontendRoot !== targetPath) {
784
+ configureVueRootPackageJson(targetPath);
785
+ }
786
+ configureVueAgents(targetPath);
787
+ if (selectedVariant === 'void') {
788
+ configureVueVoidDemoContent(targetPath);
789
+ }
790
+
791
+ const config = vueTransportConfigs[selectedVariant];
792
+ replaceInFiles(frontendRoot, {
793
+ __CREATE_FATE_OPTIONS__: config.createOptions,
794
+ __DOTENV_CONFIG__: config.dotenvConfig,
795
+ __ENV_KEYS__: config.envKeys,
796
+ __ENV_VALUES__: config.envValues,
797
+ __FATE_MODULE__: config.fateModule,
798
+ __FATE_TYPE_MODULE__: config.typeModule,
799
+ __FMT_IGNORE_PATTERNS__:
800
+ selectedVariant === 'void' || selectedVariant === 'graphql-client'
801
+ ? "['.fate/', '.void/', 'dist/', 'node_modules/', 'pnpm-lock.yaml']"
802
+ : "['.fate/', '.void/', 'dist/', 'node_modules/', '../dist/', 'pnpm-lock.yaml']",
803
+ __LINT_IGNORE_PATTERNS__:
804
+ selectedVariant === 'void' || selectedVariant === 'graphql-client'
805
+ ? "['.fate', '.void', 'dist', 'node_modules', 'vite.config.ts.timestamp-*']"
806
+ : "['.fate', '.void', 'dist', 'node_modules', '../dist', 'vite.config.ts.timestamp-*']",
807
+ __TSCONFIG_EXTENDS__:
808
+ selectedVariant === 'void' || selectedVariant === 'graphql-client'
809
+ ? './.void/tsconfig.json'
810
+ : '../tsconfig.json',
811
+ '"__TSCONFIG_INCLUDE__"':
812
+ selectedVariant === 'void'
813
+ ? `[
814
+ ".fate/**/*.ts",
815
+ ".void/**/*.d.ts",
816
+ "db/**/*.ts",
817
+ "middleware/**/*.ts",
818
+ "pages/**/*.ts",
819
+ "pages/**/*.vue",
820
+ "routes/**/*.ts",
821
+ "src/**/*.ts",
822
+ "src/**/*.vue"
823
+ ]`
824
+ : `[
825
+ ".fate/**/*.ts",
826
+ ".void/**/*.d.ts",
827
+ "pages/**/*.ts",
828
+ "pages/**/*.vue",
829
+ "src/**/*.ts",
830
+ "src/**/*.vue"
831
+ ]`,
832
+ '/* __BUILD_CONFIG__ */':
833
+ selectedVariant === 'void' || selectedVariant === 'graphql-client'
834
+ ? 'build: {},'
835
+ : "build: { outDir: join(root, '../dist/client') },",
836
+ '/* __CLIENT_IMPORTS__ */': config.clientImports,
837
+ '/* __FATE_TRANSPORT__ */': config.fateTransport,
838
+ });
839
+ };
840
+
278
841
  const copyExampleEnvFiles = (dir) => {
279
842
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
280
843
  const entryPath = path.join(dir, entry.name);
@@ -314,10 +877,19 @@ const setupProject = (targetPath, selectedVariant) => {
314
877
  runCommand('vp', ['install'], targetPath);
315
878
 
316
879
  switch (selectedVariant) {
880
+ case 'cloudflare':
881
+ runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
882
+ runCommand('vp', ['run', 'fate:generate'], targetPath);
883
+ break;
884
+ case 'graphql':
317
885
  case 'prisma':
318
886
  runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
319
887
  runCommand('vp', ['run', 'fate:generate'], targetPath);
320
888
  break;
889
+ case 'graphql-client':
890
+ runCommand('vp', ['run', 'prepare:void'], targetPath);
891
+ runCommand('vp', ['run', 'fate:generate'], targetPath);
892
+ break;
321
893
  case 'void':
322
894
  runCommand('vp', ['run', 'prepare:void'], targetPath);
323
895
  runCommand('vp', ['run', 'fate:generate'], targetPath);
@@ -368,6 +940,13 @@ const main = async () => {
368
940
  `Unknown template '${selectedVariant}'. Expected one of: ${Object.keys(variants).join(', ')}.`,
369
941
  );
370
942
  }
943
+ const selectedFramework =
944
+ options.framework ?? (interactive ? await promptForFramework() : 'react');
945
+ if (!selectedFramework || !frontendFrameworks[selectedFramework]) {
946
+ throw new Error(
947
+ `Unknown framework '${selectedFramework}'. Expected one of: ${Object.keys(frontendFrameworks).join(', ')}.`,
948
+ );
949
+ }
371
950
 
372
951
  const targetDir = validateTargetDir(
373
952
  options.targetDir ?? (interactive ? await promptForTargetDir() : 'my-app'),
@@ -378,11 +957,17 @@ const main = async () => {
378
957
  }
379
958
 
380
959
  const templateRoot = resolveTemplateRoot(variants[selectedVariant].template);
381
- const fateDependencyVersions = await resolveFateDependencyVersions(templateRoot);
382
960
  fs.mkdirSync(targetPath, { recursive: true });
383
961
  fs.cpSync(templateRoot, targetPath, { recursive: true });
962
+ if (selectedFramework === 'vue') {
963
+ configureVueTemplate(targetPath, selectedVariant);
964
+ }
384
965
  restoreTemplateFileNames(targetPath);
966
+ if (selectedFramework === 'vue') {
967
+ configureVueReadme(targetPath, selectedVariant);
968
+ }
385
969
  copyExampleEnvFiles(targetPath);
970
+ const fateDependencyVersions = await resolveFateDependencyVersions(targetPath);
386
971
  updatePackageJsonFiles(
387
972
  targetPath,
388
973
  targetPath,
@@ -394,7 +979,7 @@ const main = async () => {
394
979
  setupProject(targetPath, selectedVariant);
395
980
  }
396
981
 
397
- const message = `Created ${variants[selectedVariant].label} fate app in ${targetDir}`;
982
+ const message = `Created ${frontendFrameworks[selectedFramework].label} ${variants[selectedVariant].label} fate app in ${targetDir}`;
398
983
  if (interactive) {
399
984
  outro(message);
400
985
  } else {