From a4443ea8395b4a8785d6479fdcf3c3afbbb782e0 Mon Sep 17 00:00:00 2001
From: Eric Luce <37158449+eluce2@users.noreply.github.com>
Date: Wed, 18 Mar 2026 10:57:15 -0500
Subject: [PATCH 01/14] Add direct wrapper tests for create-proofkit
Agent-Id: agent-b0866404-cae3-459d-a9c5-f749470d81b4
---
.changeset/proofkit-cli-major-migration.md | 5 +
.github/workflows/continuous-release.yml | 2 +-
.github/workflows/release.yml | 5 +-
packages/cli-old/.yarnrc.yml | 5 +
packages/cli-old/CHANGELOG.md | 285 ++++++
packages/cli-old/README.md | 19 +
packages/cli-old/index.d.ts | 19 +
packages/cli-old/package.json | 128 +++
.../{cli => cli-old}/proofkit-cli-1.1.8.tgz | Bin
packages/cli-old/src/cli/add/auth.ts | 110 +++
.../cli/add/data-source/deploy-demo-file.ts | 96 ++
.../src/cli/add/data-source/filemaker.ts | 441 +++++++++
.../cli-old/src/cli/add/data-source/index.ts | 46 +
packages/cli-old/src/cli/add/fmschema.ts | 216 +++++
packages/cli-old/src/cli/add/index.ts | 190 ++++
packages/cli-old/src/cli/add/page/index.ts | 230 +++++
.../add/page/post-install/table-infinite.ts | 12 +
.../src/cli/add/page/post-install/table.ts | 123 +++
.../cli-old/src/cli/add/page/templates.ts | 85 ++
packages/cli-old/src/cli/add/page/types.ts | 19 +
.../src/cli/add/registry/getOptions.ts | 44 +
packages/cli-old/src/cli/add/registry/http.ts | 18 +
.../cli-old/src/cli/add/registry/install.ts | 224 +++++
.../cli-old/src/cli/add/registry/listItems.ts | 9 +
.../add/registry/postInstall/handlebars.ts | 189 ++++
.../src/cli/add/registry/postInstall/index.ts | 22 +
.../registry/postInstall/package-script.ts | 12 +
.../add/registry/postInstall/wrap-provider.ts | 132 +++
.../cli-old/src/cli/add/registry/preflight.ts | 17 +
packages/cli-old/src/cli/deploy/index.ts | 489 ++++++++++
packages/cli-old/src/cli/fmdapi.ts | 57 ++
packages/cli-old/src/cli/init.ts | 395 ++++++++
packages/cli-old/src/cli/menu.ts | 102 ++
packages/cli-old/src/cli/ottofms.ts | 268 ++++++
packages/cli-old/src/cli/prompts.ts | 188 ++++
packages/cli-old/src/cli/react-email.ts | 27 +
.../cli-old/src/cli/remove/data-source.ts | 153 +++
packages/cli-old/src/cli/remove/index.ts | 72 ++
packages/cli-old/src/cli/remove/page.ts | 214 +++++
packages/cli-old/src/cli/remove/schema.ts | 100 ++
packages/cli-old/src/cli/tanstack-query.ts | 19 +
packages/cli-old/src/cli/typegen/index.ts | 20 +
packages/cli-old/src/cli/update/index.ts | 28 +
.../src/cli/update/makeUpgradeCommand.ts | 25 +
packages/cli-old/src/cli/utils.ts | 49 +
packages/cli-old/src/consts.ts | 35 +
packages/cli-old/src/generators/auth.ts | 83 ++
packages/cli-old/src/generators/fmdapi.ts | 525 ++++++++++
packages/cli-old/src/generators/route.ts | 40 +
.../cli-old/src/generators/tanstack-query.ts | 97 ++
packages/cli-old/src/globalOptions.ts | 8 +
packages/cli-old/src/globals.d.ts | 4 +
packages/cli-old/src/helpers/createProject.ts | 129 +++
packages/cli-old/src/helpers/fmHttp.ts | 56 ++
packages/cli-old/src/helpers/git.ts | 140 +++
.../src/helpers/installDependencies.ts | 242 +++++
.../cli-old/src/helpers/installPackages.ts | 25 +
packages/cli-old/src/helpers/logNextSteps.ts | 48 +
packages/cli-old/src/helpers/replaceText.ts | 17 +
.../cli-old/src/helpers/scaffoldProject.ts | 136 +++
.../cli-old/src/helpers/selectBoilerplate.ts | 32 +
.../cli-old/src/helpers/setImportAlias.ts | 12 +
packages/cli-old/src/helpers/shadcn-cli.ts | 80 ++
packages/cli-old/src/helpers/stealth-init.ts | 20 +
.../cli-old/src/helpers/version-fetcher.ts | 131 +++
packages/cli-old/src/index.ts | 96 ++
.../cli-old/src/installers/auth-shared.ts | 49 +
.../cli-old/src/installers/better-auth.ts | 3 +
packages/cli-old/src/installers/clerk.ts | 153 +++
.../src/installers/dependencyVersionMap.ts | 108 +++
packages/cli-old/src/installers/envVars.ts | 43 +
packages/cli-old/src/installers/index.ts | 31 +
.../src/installers/install-fm-addon.ts | 53 ++
packages/cli-old/src/installers/nextAuth.ts | 189 ++++
.../cli-old/src/installers/proofkit-auth.ts | 220 +++++
.../src/installers/proofkit-webviewer.ts | 84 ++
.../cli-old/src/installers/react-email.ts | 211 +++++
packages/cli-old/src/state.ts | 33 +
packages/cli-old/src/upgrades/cursorRules.ts | 41 +
packages/cli-old/src/upgrades/index.ts | 69 ++
packages/cli-old/src/upgrades/shadcn.ts | 53 ++
.../cli-old/src/utils/addPackageDependency.ts | 32 +
packages/cli-old/src/utils/addToEnvs.ts | 131 +++
packages/cli-old/src/utils/formatting.ts | 24 +
.../cli-old/src/utils/getProofKitVersion.ts | 38 +
.../cli-old/src/utils/getUserPkgManager.ts | 21 +
packages/cli-old/src/utils/isTTYError.ts | 1 +
packages/cli-old/src/utils/logger.ts | 19 +
.../cli-old/src/utils/parseNameAndPath.ts | 42 +
packages/cli-old/src/utils/parseSettings.ts | 153 +++
.../src/utils/proofkitReleaseChannel.ts | 93 ++
.../cli-old/src/utils/removeTrailingSlash.ts | 6 +
packages/cli-old/src/utils/renderTitle.ts | 20 +
.../cli-old/src/utils/renderVersionWarning.ts | 86 ++
packages/cli-old/src/utils/ts-morph.ts | 25 +
packages/cli-old/src/utils/validateAppName.ts | 22 +
.../cli-old/src/utils/validateImportAlias.ts | 6 +
.../conditional-rules/nextjs-framework.mdc | 51 +
.../extras/_cursor/conditional-rules/npm.mdc | 60 ++
.../extras/_cursor/conditional-rules/pnpm.mdc | 65 ++
.../extras/_cursor/conditional-rules/yarn.mdc | 60 ++
.../extras/_cursor/rules/cursor-rules.mdc | 88 ++
.../extras/_cursor/rules/filemaker-api.mdc | 176 ++++
.../rules/troubleshooting-patterns.mdc | 240 +++++
.../extras/_cursor/rules/ui-components.mdc | 57 ++
.../extras/config/drizzle-config-mysql.ts | 12 +
.../extras/config/drizzle-config-postgres.ts | 12 +
.../extras/config/drizzle-config-sqlite.ts | 12 +
.../extras/config/fmschema.config.mjs | 9 +
.../extras/config/get-query-client.ts | 6 +
.../template/extras/config/postcss.config.cjs | 7 +
.../extras/config/query-provider-vite.tsx | 17 +
.../template/extras/config/query-provider.tsx | 21 +
.../extras/emailProviders/none/email.tsx | 24 +
.../extras/emailProviders/plunk/email.tsx | 27 +
.../extras/emailProviders/plunk/service.ts | 4 +
.../extras/emailProviders/resend/email.tsx | 24 +
.../extras/emailProviders/resend/service.ts | 4 +
.../extras/emailTemplates/auth-code.tsx | 137 +++
.../extras/emailTemplates/generic.tsx | 113 +++
.../app/(main)/auth/profile/actions.ts | 97 ++
.../app/(main)/auth/profile/page.tsx | 29 +
.../app/(main)/auth/profile/profile-form.tsx | 58 ++
.../auth/profile/reset-password-form.tsx | 112 +++
.../app/(main)/auth/profile/schema.ts | 19 +
.../app/auth/forgot-password/actions.ts | 39 +
.../app/auth/forgot-password/forgot-form.tsx | 42 +
.../app/auth/forgot-password/page.tsx | 22 +
.../app/auth/forgot-password/schema.ts | 5 +
.../fmaddon-auth/app/auth/login/actions.ts | 35 +
.../app/auth/login/login-form.tsx | 66 ++
.../fmaddon-auth/app/auth/login/page.tsx | 27 +
.../fmaddon-auth/app/auth/login/schema.ts | 6 +
.../app/auth/reset-password/actions.ts | 53 ++
.../app/auth/reset-password/page.tsx | 33 +
.../reset-password/reset-password-form.tsx | 60 ++
.../app/auth/reset-password/schema.ts | 14 +
.../reset-password/verify-email/actions.ts | 46 +
.../auth/reset-password/verify-email/page.tsx | 33 +
.../reset-password/verify-email/schema.ts | 5 +
.../verify-email/verify-email-form.tsx | 49 +
.../fmaddon-auth/app/auth/signup/actions.ts | 50 +
.../fmaddon-auth/app/auth/signup/page.tsx | 27 +
.../fmaddon-auth/app/auth/signup/schema.ts | 12 +
.../app/auth/signup/signup-form.tsx | 68 ++
.../app/auth/verify-email/actions.ts | 109 +++
.../verify-email/email-verification-form.tsx | 46 +
.../app/auth/verify-email/page.tsx | 40 +
.../app/auth/verify-email/resend-button.tsx | 37 +
.../app/auth/verify-email/schema.ts | 5 +
.../fmaddon-auth/components/auth/actions.ts | 19 +
.../fmaddon-auth/components/auth/protect.tsx | 18 +
.../fmaddon-auth/components/auth/redirect.tsx | 26 +
.../fmaddon-auth/components/auth/use-user.ts | 60 ++
.../components/auth/user-menu.tsx | 52 +
.../extras/fmaddon-auth/emails/auth-code.tsx | 137 +++
.../extras/fmaddon-auth/middleware.ts | 44 +
.../server/auth/utils/email-verification.ts | 137 +++
.../server/auth/utils/encryption.ts | 51 +
.../fmaddon-auth/server/auth/utils/index.ts | 16 +
.../server/auth/utils/password-reset.ts | 153 +++
.../server/auth/utils/password.ts | 67 ++
.../server/auth/utils/redirect.ts | 8 +
.../fmaddon-auth/server/auth/utils/session.ts | 191 ++++
.../fmaddon-auth/server/auth/utils/user.ts | 146 +++
.../prisma/schema/base-planetscale.prisma | 24 +
.../template/extras/prisma/schema/base.prisma | 20 +
.../schema/with-auth-planetscale.prisma | 77 ++
.../extras/prisma/schema/with-auth.prisma | 74 ++
.../extras/src/app/_components/post-tw.tsx | 50 +
.../extras/src/app/_components/post.tsx | 54 ++
.../src/app/api/auth/[...nextauth]/route.ts | 4 +
.../extras/src/app/api/trpc/[trpc]/route.ts | 34 +
.../extras/src/app/clerk-auth/layout.tsx | 10 +
.../clerk-auth/signin/[[...sign-in]]/page.tsx | 5 +
.../clerk-auth/signup/[[...sign-up]]/page.tsx | 5 +
.../template/extras/src/app/layout/base.tsx | 34 +
.../extras/src/app/layout/main-shell.tsx | 37 +
.../extras/src/app/layout/with-trpc-tw.tsx | 24 +
.../extras/src/app/layout/with-trpc.tsx | 24 +
.../extras/src/app/layout/with-tw.tsx | 20 +
.../extras/src/app/next-auth/layout.tsx | 22 +
.../extras/src/app/next-auth/signin/page.tsx | 83 ++
.../extras/src/app/next-auth/signup/action.ts | 24 +
.../extras/src/app/next-auth/signup/page.tsx | 40 +
.../src/app/next-auth/signup/validation.ts | 12 +
.../template/extras/src/app/page/base.tsx | 6 +
.../extras/src/app/page/with-auth-trpc-tw.tsx | 67 ++
.../extras/src/app/page/with-auth-trpc.tsx | 68 ++
.../extras/src/app/page/with-trpc-tw.tsx | 53 ++
.../extras/src/app/page/with-trpc.tsx | 54 ++
.../template/extras/src/app/page/with-tw.tsx | 37 +
.../components/clerk-auth/clerk-provider.tsx | 18 +
.../clerk-auth/user-menu-mobile.tsx | 36 +
.../src/components/clerk-auth/user-menu.tsx | 24 +
.../next-auth/next-auth-provider.tsx | 14 +
.../components/next-auth/user-menu-mobile.tsx | 31 +
.../src/components/next-auth/user-menu.tsx | 38 +
.../template/extras/src/env/with-auth.ts | 31 +
.../template/extras/src/env/with-clerk.ts | 20 +
.../template/extras/src/index.module.css | 177 ++++
.../template/extras/src/middleware/clerk.ts | 20 +
.../extras/src/middleware/next-auth.ts | 5 +
.../template/extras/src/pages/_app/base.tsx | 14 +
.../src/pages/_app/with-auth-trpc-tw.tsx | 23 +
.../extras/src/pages/_app/with-auth-trpc.tsx | 23 +
.../extras/src/pages/_app/with-auth-tw.tsx | 21 +
.../extras/src/pages/_app/with-auth.tsx | 21 +
.../extras/src/pages/_app/with-trpc-tw.tsx | 16 +
.../extras/src/pages/_app/with-trpc.tsx | 16 +
.../extras/src/pages/_app/with-tw.tsx | 14 +
.../src/pages/api/auth/[...nextauth].ts | 5 +
.../extras/src/pages/api/trpc/[trpc].ts | 19 +
.../template/extras/src/pages/index/base.tsx | 47 +
.../src/pages/index/with-auth-trpc-tw.tsx | 80 ++
.../extras/src/pages/index/with-auth-trpc.tsx | 81 ++
.../extras/src/pages/index/with-trpc-tw.tsx | 52 +
.../extras/src/pages/index/with-trpc.tsx | 53 ++
.../extras/src/pages/index/with-tw.tsx | 45 +
.../template/extras/src/server/api/root.ts | 23 +
.../src/server/api/routers/post/base.ts | 40 +
.../api/routers/post/with-auth-drizzle.ts | 39 +
.../api/routers/post/with-auth-prisma.ts | 41 +
.../src/server/api/routers/post/with-auth.ts | 37 +
.../server/api/routers/post/with-drizzle.ts | 30 +
.../server/api/routers/post/with-prisma.ts | 31 +
.../extras/src/server/api/trpc-app/base.ts | 103 ++
.../src/server/api/trpc-app/with-auth-db.ts | 133 +++
.../src/server/api/trpc-app/with-auth.ts | 130 +++
.../extras/src/server/api/trpc-app/with-db.ts | 106 +++
.../extras/src/server/api/trpc-pages/base.ts | 122 +++
.../src/server/api/trpc-pages/with-auth-db.ts | 160 ++++
.../src/server/api/trpc-pages/with-auth.ts | 158 +++
.../src/server/api/trpc-pages/with-db.ts | 125 +++
.../template/extras/src/server/data/users.ts | 23 +
.../src/server/db/db-prisma-planetscale.ts | 22 +
.../extras/src/server/db/db-prisma.ts | 17 +
.../src/server/db/index-drizzle/with-mysql.ts | 18 +
.../db/index-drizzle/with-planetscale.ts | 7 +
.../server/db/index-drizzle/with-postgres.ts | 18 +
.../server/db/index-drizzle/with-sqlite.ts | 19 +
.../server/db/schema-drizzle/base-mysql.ts | 34 +
.../db/schema-drizzle/base-planetscale.ts | 34 +
.../server/db/schema-drizzle/base-postgres.ts | 36 +
.../server/db/schema-drizzle/base-sqlite.ts | 30 +
.../db/schema-drizzle/with-auth-mysql.ts | 123 +++
.../schema-drizzle/with-auth-planetscale.ts | 117 +++
.../db/schema-drizzle/with-auth-postgres.ts | 130 +++
.../db/schema-drizzle/with-auth-sqlite.ts | 116 +++
.../extras/src/server/next-auth/base.ts | 111 +++
.../extras/src/server/next-auth/password.ts | 13 +
.../src/server/next-auth/with-drizzle.ts | 83 ++
.../src/server/next-auth/with-prisma.ts | 72 ++
.../template/extras/src/trpc/query-client.ts | 25 +
.../template/extras/src/trpc/react.tsx | 76 ++
.../template/extras/src/trpc/server.ts | 30 +
.../cli-old/template/extras/src/utils/api.ts | 68 ++
.../template/extras/start-database/mysql.sh | 54 ++
.../extras/start-database/postgres.sh | 55 ++
.../template/fm-addon/ProofKitAuth/de.xml | 518 ++++++++++
.../template/fm-addon/ProofKitAuth/en.xml | 518 ++++++++++
.../template/fm-addon/ProofKitAuth/es.xml | 518 ++++++++++
.../template/fm-addon/ProofKitAuth/fr.xml | 518 ++++++++++
.../template/fm-addon/ProofKitAuth/icon.png | Bin 0 -> 38399 bytes
.../fm-addon/ProofKitAuth/icon@2x.png | Bin 0 -> 38399 bytes
.../template/fm-addon/ProofKitAuth/info.json | 8 +
.../fm-addon/ProofKitAuth/info_de.json | 11 +
.../fm-addon/ProofKitAuth/info_en.json | 8 +
.../fm-addon/ProofKitAuth/info_es.json | 11 +
.../fm-addon/ProofKitAuth/info_fr.json | 11 +
.../fm-addon/ProofKitAuth/info_it.json | 11 +
.../fm-addon/ProofKitAuth/info_ja.json | 11 +
.../fm-addon/ProofKitAuth/info_ko.json | 11 +
.../fm-addon/ProofKitAuth/info_nl.json | 11 +
.../fm-addon/ProofKitAuth/info_pt.json | 11 +
.../fm-addon/ProofKitAuth/info_sv.json | 11 +
.../fm-addon/ProofKitAuth/info_zh.json | 11 +
.../template/fm-addon/ProofKitAuth/it.xml | 518 ++++++++++
.../template/fm-addon/ProofKitAuth/ja.xml | 518 ++++++++++
.../template/fm-addon/ProofKitAuth/ko.xml | 518 ++++++++++
.../template/fm-addon/ProofKitAuth/nl.xml | 518 ++++++++++
.../fm-addon/ProofKitAuth/preview.png | Bin 0 -> 38399 bytes
.../template/fm-addon/ProofKitAuth/pt.xml | 518 ++++++++++
.../template/fm-addon/ProofKitAuth/sv.xml | 518 ++++++++++
.../fm-addon/ProofKitAuth/template.xml | Bin 0 -> 938018 bytes
.../template/fm-addon/ProofKitAuth/zh.xml | 518 ++++++++++
.../template/fm-addon/ProofKitWV/de.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/en.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/es.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/fr.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/icon.png | Bin 0 -> 44003 bytes
.../template/fm-addon/ProofKitWV/icon@2x.png | Bin 0 -> 44003 bytes
.../template/fm-addon/ProofKitWV/info.json | 8 +
.../template/fm-addon/ProofKitWV/info_de.json | 11 +
.../template/fm-addon/ProofKitWV/info_en.json | 7 +
.../template/fm-addon/ProofKitWV/info_es.json | 11 +
.../template/fm-addon/ProofKitWV/info_fr.json | 11 +
.../template/fm-addon/ProofKitWV/info_it.json | 11 +
.../template/fm-addon/ProofKitWV/info_ja.json | 11 +
.../template/fm-addon/ProofKitWV/info_ko.json | 11 +
.../template/fm-addon/ProofKitWV/info_nl.json | 11 +
.../template/fm-addon/ProofKitWV/info_pt.json | 11 +
.../template/fm-addon/ProofKitWV/info_sv.json | 11 +
.../template/fm-addon/ProofKitWV/info_zh.json | 11 +
.../template/fm-addon/ProofKitWV/it.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/ja.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/ko.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/nl.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/preview.png | Bin 0 -> 44003 bytes
.../template/fm-addon/ProofKitWV/pt.xml | 896 ++++++++++++++++++
.../fm-addon/ProofKitWV/records_de.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_en.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_es.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_fr.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_it.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_ja.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_ko.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_nl.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_pt.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_sv.xml | Bin 0 -> 1918 bytes
.../fm-addon/ProofKitWV/records_zh.xml | Bin 0 -> 1918 bytes
.../template/fm-addon/ProofKitWV/sv.xml | 896 ++++++++++++++++++
.../template/fm-addon/ProofKitWV/template.xml | Bin 0 -> 237896 bytes
.../template/fm-addon/ProofKitWV/zh.xml | 896 ++++++++++++++++++
.../cli-old/template/nextjs-mantine/README.md | 27 +
.../template/nextjs-mantine/_gitignore | 37 +
.../template/nextjs-mantine/components.json | 21 +
.../template/nextjs-mantine/next.config.ts | 12 +
.../template/nextjs-mantine/package.json | 51 +
.../nextjs-mantine/postcss.config.cjs | 15 +
.../template/nextjs-mantine/proofkit.json | 7 +
.../nextjs-mantine/public/favicon.ico | Bin 0 -> 15086 bytes
.../nextjs-mantine/public/proofkit.png | Bin 0 -> 52140 bytes
.../nextjs-mantine/src/app/(main)/layout.tsx | 6 +
.../nextjs-mantine/src/app/(main)/page.tsx | 90 ++
.../nextjs-mantine/src/app/layout.tsx | 39 +
.../nextjs-mantine/src/app/navigation.tsx | 12 +
.../nextjs-mantine/src/components/AppLogo.tsx | 6 +
.../components/AppShell/internal/AppShell.tsx | 21 +
.../AppShell/internal/Header.module.css | 40 +
.../components/AppShell/internal/Header.tsx | 34 +
.../AppShell/internal/HeaderMobileMenu.tsx | 27 +
.../AppShell/internal/HeaderNavLink.tsx | 35 +
.../components/AppShell/internal/config.ts | 1 +
.../AppShell/slot-header-center.tsx | 13 +
.../components/AppShell/slot-header-left.tsx | 23 +
.../AppShell/slot-header-mobile-content.tsx | 43 +
.../components/AppShell/slot-header-right.tsx | 26 +
.../template/nextjs-mantine/src/config/env.ts | 13 +
.../src/config/theme/globals.css | 125 +++
.../src/config/theme/mantine-theme.ts | 22 +
.../nextjs-mantine/src/server/safe-action.ts | 3 +
.../src/utils/notification-helpers.ts | 32 +
.../nextjs-mantine/src/utils/styles.ts | 6 +
.../template/nextjs-mantine/tsconfig.json | 27 +
.../template/nextjs-shadcn/.claude/CLAUDE.md | 327 +++++++
.../nextjs-shadcn/.cursor/rules/ultracite.mdc | 333 +++++++
.../nextjs-shadcn/.vscode/settings.json | 35 +
.../cli-old/template/nextjs-shadcn/README.md | 27 +
.../cli-old/template/nextjs-shadcn/_gitignore | 37 +
.../cli-old/template/nextjs-shadcn/biome.json | 48 +
.../template/nextjs-shadcn/components.json | 21 +
.../template/nextjs-shadcn/next.config.ts | 8 +
.../template/nextjs-shadcn/package.json | 38 +
.../template/nextjs-shadcn/postcss.config.mjs | 5 +
.../template/nextjs-shadcn/proofkit.json | 6 +
.../template/nextjs-shadcn/public/favicon.ico | Bin 0 -> 15086 bytes
.../nextjs-shadcn/public/proofkit.png | Bin 0 -> 52140 bytes
.../nextjs-shadcn/src/app/(main)/layout.tsx | 6 +
.../nextjs-shadcn/src/app/(main)/page.tsx | 124 +++
.../nextjs-shadcn/src/app/globals.css | 122 +++
.../template/nextjs-shadcn/src/app/layout.tsx | 35 +
.../nextjs-shadcn/src/app/navigation.tsx | 12 +
.../nextjs-shadcn/src/components/AppLogo.tsx | 6 +
.../components/AppShell/internal/AppShell.tsx | 23 +
.../AppShell/internal/Header.module.css | 33 +
.../components/AppShell/internal/Header.tsx | 30 +
.../AppShell/internal/HeaderMobileMenu.tsx | 25 +
.../AppShell/internal/HeaderNavLink.tsx | 35 +
.../components/AppShell/internal/config.ts | 1 +
.../AppShell/slot-header-center.tsx | 13 +
.../components/AppShell/slot-header-left.tsx | 23 +
.../AppShell/slot-header-mobile-content.tsx | 43 +
.../components/AppShell/slot-header-right.tsx | 25 +
.../src/components/mode-toggle.tsx | 39 +
.../src/components/providers.tsx | 13 +
.../src/components/theme-provider.tsx | 11 +
.../src/components/ui/button.tsx | 61 ++
.../src/components/ui/dropdown-menu.tsx | 267 ++++++
.../src/components/ui/sonner.tsx | 31 +
.../template/nextjs-shadcn/src/lib/env.ts | 12 +
.../template/nextjs-shadcn/src/lib/utils.ts | 6 +
.../template/nextjs-shadcn/tsconfig.json | 41 +
.../template/pages/nextjs/blank/page.tsx | 5 +
.../pages/nextjs/table-edit/actions.ts | 24 +
.../template/pages/nextjs/table-edit/page.tsx | 28 +
.../pages/nextjs/table-edit/schema.ts | 4 +
.../pages/nextjs/table-edit/table.tsx | 45 +
.../nextjs/table-infinite-edit/actions.ts | 84 ++
.../pages/nextjs/table-infinite-edit/page.tsx | 23 +
.../pages/nextjs/table-infinite-edit/query.ts | 87 ++
.../nextjs/table-infinite-edit/schema.ts | 4 +
.../nextjs/table-infinite-edit/table.tsx | 130 +++
.../pages/nextjs/table-infinite/actions.ts | 62 ++
.../pages/nextjs/table-infinite/page.tsx | 11 +
.../pages/nextjs/table-infinite/query.ts | 45 +
.../pages/nextjs/table-infinite/table.tsx | 108 +++
.../template/pages/nextjs/table/page.tsx | 17 +
.../template/pages/nextjs/table/table.tsx | 18 +
.../template/pages/vite-wv/blank/index.tsx | 0
.../pages/vite-wv/table-edit/index.tsx | 72 ++
.../template/pages/vite-wv/table/index.tsx | 35 +
.../template/vite-wv/.claude/launch.json | 18 +
.../template/vite-wv/.vscode/settings.json | 11 +
packages/cli-old/template/vite-wv/AGENTS.md | 1 +
packages/cli-old/template/vite-wv/CLAUDE.md | 1 +
packages/cli-old/template/vite-wv/_gitignore | 19 +
.../cli-old/template/vite-wv/components.json | 21 +
packages/cli-old/template/vite-wv/index.html | 13 +
.../cli-old/template/vite-wv/package.json | 38 +
.../vite-wv/proofkit-typegen.config.jsonc | 18 +
.../cli-old/template/vite-wv/proofkit.json | 9 +
.../template/vite-wv/scripts/filemaker.js | 96 ++
.../template/vite-wv/scripts/launch-fm.js | 19 +
.../template/vite-wv/scripts/upload.js | 24 +
packages/cli-old/template/vite-wv/src/App.tsx | 84 ++
.../cli-old/template/vite-wv/src/index.css | 96 ++
.../cli-old/template/vite-wv/src/lib/utils.ts | 6 +
.../cli-old/template/vite-wv/src/main.tsx | 21 +
.../cli-old/template/vite-wv/src/router.tsx | 57 ++
.../vite-wv/src/routes/query-demo.tsx | 37 +
.../cli-old/template/vite-wv/tsconfig.json | 16 +
.../cli-old/template/vite-wv/vite.config.ts | 18 +
.../tests/browser-apps.smoke.test.ts} | 8 +-
packages/cli-old/tests/cli.test.ts | 22 +
.../init-non-interactive-failures.test.ts | 222 +++++
.../init-post-init-generation-errors.test.ts | 62 ++
.../tests/init-run-init-regression.test.ts | 197 ++++
.../tests/init-scaffold-contract.test.ts | 220 +++++
packages/cli-old/tests/setup.ts | 13 +
packages/cli-old/tests/test-utils.ts | 70 ++
packages/cli-old/tests/webviewer-apps.test.ts | 155 +++
packages/cli-old/tsconfig.json | 14 +
packages/cli-old/tsdown.config.ts | 54 ++
packages/cli-old/vitest.config.ts | 24 +
packages/cli-old/vitest.smoke.config.ts | 18 +
packages/cli/package.json | 23 +-
packages/cli/src/cli/init.ts | 106 ++-
packages/cli/src/consts.ts | 50 +-
packages/{new => cli}/src/core/context.ts | 22 +-
packages/{new => cli}/src/core/errors.ts | 0
.../{new => cli}/src/core/executeInitPlan.ts | 51 +-
packages/{new => cli}/src/core/planInit.ts | 7 +-
.../src/core/resolveInitRequest.ts | 12 +
packages/{new => cli}/src/core/types.ts | 0
packages/cli/src/index.ts | 474 +++++++--
packages/{new => cli}/src/services/live.ts | 0
.../{new => cli}/src/utils/browserOpen.ts | 0
packages/{new => cli}/src/utils/http.ts | 0
.../{new => cli}/src/utils/packageManager.ts | 0
.../{new => cli}/src/utils/projectFiles.ts | 35 +-
.../{new => cli}/src/utils/projectName.ts | 0
packages/{new => cli}/src/utils/prompts.ts | 0
packages/cli/src/utils/renderTitle.ts | 14 +-
packages/{new => cli}/src/utils/versioning.ts | 0
packages/cli/tests/browser-apps.smoke.test.ts | 87 ++
packages/cli/tests/cli.test.ts | 117 ++-
.../tests/default-command.test.ts | 6 +-
packages/{new => cli}/tests/executor.test.ts | 0
packages/{new => cli}/tests/init-fixtures.ts | 6 +
.../init-non-interactive-failures.test.ts | 222 +++++
.../init-post-init-generation-errors.test.ts | 62 ++
.../tests/init-run-init-regression.test.ts | 197 ++++
.../cli/tests/init-scaffold-contract.test.ts | 228 +++++
.../{new => cli}/tests/integration.test.ts | 68 +-
packages/{new => cli}/tests/planner.test.ts | 1 +
.../{new => cli}/tests/project-name.test.ts | 0
packages/{new => cli}/tests/prompts.test.ts | 0
.../{new => cli}/tests/resolve-init.test.ts | 0
packages/{new => cli}/tests/test-layer.ts | 0
packages/cli/tests/test-utils.ts | 12 +-
packages/cli/tests/webviewer-apps.test.ts | 39 +-
packages/cli/tsconfig.json | 8 +-
packages/cli/tsdown.config.ts | 41 -
packages/cli/vitest.config.ts | 12 +-
.../vitest.smoke.config.ts} | 3 +-
packages/create-proofkit/package.json | 6 +-
packages/create-proofkit/src/index.js | 13 +-
packages/create-proofkit/tests/index.test.js | 104 ++
packages/create-proofkit/vitest.config.ts | 8 +
packages/new/package.json | 65 --
packages/new/src/utils/renderTitle.ts | 19 -
packages/new/tests/cli.test.ts | 70 --
packages/new/tsconfig.json | 12 -
packages/new/tsdown.config.ts | 14 -
pnpm-lock.yaml | 564 ++++++-----
496 files changed, 40198 insertions(+), 659 deletions(-)
create mode 100644 .changeset/proofkit-cli-major-migration.md
create mode 100644 packages/cli-old/.yarnrc.yml
create mode 100644 packages/cli-old/CHANGELOG.md
create mode 100644 packages/cli-old/README.md
create mode 100644 packages/cli-old/index.d.ts
create mode 100644 packages/cli-old/package.json
rename packages/{cli => cli-old}/proofkit-cli-1.1.8.tgz (100%)
create mode 100644 packages/cli-old/src/cli/add/auth.ts
create mode 100644 packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts
create mode 100644 packages/cli-old/src/cli/add/data-source/filemaker.ts
create mode 100644 packages/cli-old/src/cli/add/data-source/index.ts
create mode 100644 packages/cli-old/src/cli/add/fmschema.ts
create mode 100644 packages/cli-old/src/cli/add/index.ts
create mode 100644 packages/cli-old/src/cli/add/page/index.ts
create mode 100644 packages/cli-old/src/cli/add/page/post-install/table-infinite.ts
create mode 100644 packages/cli-old/src/cli/add/page/post-install/table.ts
create mode 100644 packages/cli-old/src/cli/add/page/templates.ts
create mode 100644 packages/cli-old/src/cli/add/page/types.ts
create mode 100644 packages/cli-old/src/cli/add/registry/getOptions.ts
create mode 100644 packages/cli-old/src/cli/add/registry/http.ts
create mode 100644 packages/cli-old/src/cli/add/registry/install.ts
create mode 100644 packages/cli-old/src/cli/add/registry/listItems.ts
create mode 100644 packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts
create mode 100644 packages/cli-old/src/cli/add/registry/postInstall/index.ts
create mode 100644 packages/cli-old/src/cli/add/registry/postInstall/package-script.ts
create mode 100644 packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts
create mode 100644 packages/cli-old/src/cli/add/registry/preflight.ts
create mode 100644 packages/cli-old/src/cli/deploy/index.ts
create mode 100644 packages/cli-old/src/cli/fmdapi.ts
create mode 100644 packages/cli-old/src/cli/init.ts
create mode 100644 packages/cli-old/src/cli/menu.ts
create mode 100644 packages/cli-old/src/cli/ottofms.ts
create mode 100644 packages/cli-old/src/cli/prompts.ts
create mode 100644 packages/cli-old/src/cli/react-email.ts
create mode 100644 packages/cli-old/src/cli/remove/data-source.ts
create mode 100644 packages/cli-old/src/cli/remove/index.ts
create mode 100644 packages/cli-old/src/cli/remove/page.ts
create mode 100644 packages/cli-old/src/cli/remove/schema.ts
create mode 100644 packages/cli-old/src/cli/tanstack-query.ts
create mode 100644 packages/cli-old/src/cli/typegen/index.ts
create mode 100644 packages/cli-old/src/cli/update/index.ts
create mode 100644 packages/cli-old/src/cli/update/makeUpgradeCommand.ts
create mode 100644 packages/cli-old/src/cli/utils.ts
create mode 100644 packages/cli-old/src/consts.ts
create mode 100644 packages/cli-old/src/generators/auth.ts
create mode 100644 packages/cli-old/src/generators/fmdapi.ts
create mode 100644 packages/cli-old/src/generators/route.ts
create mode 100644 packages/cli-old/src/generators/tanstack-query.ts
create mode 100644 packages/cli-old/src/globalOptions.ts
create mode 100644 packages/cli-old/src/globals.d.ts
create mode 100644 packages/cli-old/src/helpers/createProject.ts
create mode 100644 packages/cli-old/src/helpers/fmHttp.ts
create mode 100644 packages/cli-old/src/helpers/git.ts
create mode 100644 packages/cli-old/src/helpers/installDependencies.ts
create mode 100644 packages/cli-old/src/helpers/installPackages.ts
create mode 100644 packages/cli-old/src/helpers/logNextSteps.ts
create mode 100644 packages/cli-old/src/helpers/replaceText.ts
create mode 100644 packages/cli-old/src/helpers/scaffoldProject.ts
create mode 100644 packages/cli-old/src/helpers/selectBoilerplate.ts
create mode 100644 packages/cli-old/src/helpers/setImportAlias.ts
create mode 100644 packages/cli-old/src/helpers/shadcn-cli.ts
create mode 100644 packages/cli-old/src/helpers/stealth-init.ts
create mode 100644 packages/cli-old/src/helpers/version-fetcher.ts
create mode 100644 packages/cli-old/src/index.ts
create mode 100644 packages/cli-old/src/installers/auth-shared.ts
create mode 100644 packages/cli-old/src/installers/better-auth.ts
create mode 100644 packages/cli-old/src/installers/clerk.ts
create mode 100644 packages/cli-old/src/installers/dependencyVersionMap.ts
create mode 100644 packages/cli-old/src/installers/envVars.ts
create mode 100644 packages/cli-old/src/installers/index.ts
create mode 100644 packages/cli-old/src/installers/install-fm-addon.ts
create mode 100644 packages/cli-old/src/installers/nextAuth.ts
create mode 100644 packages/cli-old/src/installers/proofkit-auth.ts
create mode 100644 packages/cli-old/src/installers/proofkit-webviewer.ts
create mode 100644 packages/cli-old/src/installers/react-email.ts
create mode 100644 packages/cli-old/src/state.ts
create mode 100644 packages/cli-old/src/upgrades/cursorRules.ts
create mode 100644 packages/cli-old/src/upgrades/index.ts
create mode 100644 packages/cli-old/src/upgrades/shadcn.ts
create mode 100644 packages/cli-old/src/utils/addPackageDependency.ts
create mode 100644 packages/cli-old/src/utils/addToEnvs.ts
create mode 100644 packages/cli-old/src/utils/formatting.ts
create mode 100644 packages/cli-old/src/utils/getProofKitVersion.ts
create mode 100644 packages/cli-old/src/utils/getUserPkgManager.ts
create mode 100644 packages/cli-old/src/utils/isTTYError.ts
create mode 100644 packages/cli-old/src/utils/logger.ts
create mode 100644 packages/cli-old/src/utils/parseNameAndPath.ts
create mode 100644 packages/cli-old/src/utils/parseSettings.ts
create mode 100644 packages/cli-old/src/utils/proofkitReleaseChannel.ts
create mode 100644 packages/cli-old/src/utils/removeTrailingSlash.ts
create mode 100644 packages/cli-old/src/utils/renderTitle.ts
create mode 100644 packages/cli-old/src/utils/renderVersionWarning.ts
create mode 100644 packages/cli-old/src/utils/ts-morph.ts
create mode 100644 packages/cli-old/src/utils/validateAppName.ts
create mode 100644 packages/cli-old/src/utils/validateImportAlias.ts
create mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc
create mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc
create mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc
create mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc
create mode 100644 packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc
create mode 100644 packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc
create mode 100644 packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc
create mode 100644 packages/cli-old/template/extras/_cursor/rules/ui-components.mdc
create mode 100644 packages/cli-old/template/extras/config/drizzle-config-mysql.ts
create mode 100644 packages/cli-old/template/extras/config/drizzle-config-postgres.ts
create mode 100644 packages/cli-old/template/extras/config/drizzle-config-sqlite.ts
create mode 100644 packages/cli-old/template/extras/config/fmschema.config.mjs
create mode 100644 packages/cli-old/template/extras/config/get-query-client.ts
create mode 100644 packages/cli-old/template/extras/config/postcss.config.cjs
create mode 100644 packages/cli-old/template/extras/config/query-provider-vite.tsx
create mode 100644 packages/cli-old/template/extras/config/query-provider.tsx
create mode 100644 packages/cli-old/template/extras/emailProviders/none/email.tsx
create mode 100644 packages/cli-old/template/extras/emailProviders/plunk/email.tsx
create mode 100644 packages/cli-old/template/extras/emailProviders/plunk/service.ts
create mode 100644 packages/cli-old/template/extras/emailProviders/resend/email.tsx
create mode 100644 packages/cli-old/template/extras/emailProviders/resend/service.ts
create mode 100644 packages/cli-old/template/extras/emailTemplates/auth-code.tsx
create mode 100644 packages/cli-old/template/extras/emailTemplates/generic.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/middleware.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts
create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts
create mode 100644 packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma
create mode 100644 packages/cli-old/template/extras/prisma/schema/base.prisma
create mode 100644 packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma
create mode 100644 packages/cli-old/template/extras/prisma/schema/with-auth.prisma
create mode 100644 packages/cli-old/template/extras/src/app/_components/post-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/app/_components/post.tsx
create mode 100644 packages/cli-old/template/extras/src/app/api/auth/[...nextauth]/route.ts
create mode 100644 packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts
create mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx
create mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx
create mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx
create mode 100644 packages/cli-old/template/extras/src/app/layout/base.tsx
create mode 100644 packages/cli-old/template/extras/src/app/layout/main-shell.tsx
create mode 100644 packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/app/layout/with-trpc.tsx
create mode 100644 packages/cli-old/template/extras/src/app/layout/with-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/app/next-auth/layout.tsx
create mode 100644 packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx
create mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/action.ts
create mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx
create mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts
create mode 100644 packages/cli-old/template/extras/src/app/page/base.tsx
create mode 100644 packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx
create mode 100644 packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/app/page/with-trpc.tsx
create mode 100644 packages/cli-old/template/extras/src/app/page/with-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx
create mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx
create mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx
create mode 100644 packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx
create mode 100644 packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx
create mode 100644 packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx
create mode 100644 packages/cli-old/template/extras/src/env/with-auth.ts
create mode 100644 packages/cli-old/template/extras/src/env/with-clerk.ts
create mode 100644 packages/cli-old/template/extras/src/index.module.css
create mode 100644 packages/cli-old/template/extras/src/middleware/clerk.ts
create mode 100644 packages/cli-old/template/extras/src/middleware/next-auth.ts
create mode 100644 packages/cli-old/template/extras/src/pages/_app/base.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts
create mode 100644 packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts
create mode 100644 packages/cli-old/template/extras/src/pages/index/base.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/index/with-trpc.tsx
create mode 100644 packages/cli-old/template/extras/src/pages/index/with-tw.tsx
create mode 100644 packages/cli-old/template/extras/src/server/api/root.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/base.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/base.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts
create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts
create mode 100644 packages/cli-old/template/extras/src/server/data/users.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/db-prisma.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts
create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts
create mode 100644 packages/cli-old/template/extras/src/server/next-auth/base.ts
create mode 100644 packages/cli-old/template/extras/src/server/next-auth/password.ts
create mode 100644 packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts
create mode 100644 packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts
create mode 100644 packages/cli-old/template/extras/src/trpc/query-client.ts
create mode 100644 packages/cli-old/template/extras/src/trpc/react.tsx
create mode 100644 packages/cli-old/template/extras/src/trpc/server.ts
create mode 100644 packages/cli-old/template/extras/src/utils/api.ts
create mode 100755 packages/cli-old/template/extras/start-database/mysql.sh
create mode 100755 packages/cli-old/template/extras/start-database/postgres.sh
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/de.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/en.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/es.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/fr.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/icon.png
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/icon@2x.png
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_de.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_en.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_es.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_fr.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_it.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_ja.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_ko.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_nl.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_pt.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_sv.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_zh.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/it.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/ja.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/ko.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/nl.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/preview.png
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/pt.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/sv.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/template.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/zh.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/de.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/en.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/es.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/fr.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/icon.png
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/icon@2x.png
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_de.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_en.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_es.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_fr.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_it.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_ja.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_ko.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_nl.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_pt.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_sv.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_zh.json
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/it.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/ja.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/ko.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/nl.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/preview.png
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/pt.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_de.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_en.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_es.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_fr.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_it.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_ja.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_ko.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_nl.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_pt.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_sv.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_zh.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/sv.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/template.xml
create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/zh.xml
create mode 100644 packages/cli-old/template/nextjs-mantine/README.md
create mode 100644 packages/cli-old/template/nextjs-mantine/_gitignore
create mode 100644 packages/cli-old/template/nextjs-mantine/components.json
create mode 100644 packages/cli-old/template/nextjs-mantine/next.config.ts
create mode 100644 packages/cli-old/template/nextjs-mantine/package.json
create mode 100644 packages/cli-old/template/nextjs-mantine/postcss.config.cjs
create mode 100644 packages/cli-old/template/nextjs-mantine/proofkit.json
create mode 100644 packages/cli-old/template/nextjs-mantine/public/favicon.ico
create mode 100644 packages/cli-old/template/nextjs-mantine/public/proofkit.png
create mode 100644 packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/app/layout.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx
create mode 100644 packages/cli-old/template/nextjs-mantine/src/config/env.ts
create mode 100644 packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css
create mode 100644 packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts
create mode 100644 packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts
create mode 100644 packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts
create mode 100644 packages/cli-old/template/nextjs-mantine/src/utils/styles.ts
create mode 100644 packages/cli-old/template/nextjs-mantine/tsconfig.json
create mode 100644 packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md
create mode 100644 packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc
create mode 100644 packages/cli-old/template/nextjs-shadcn/.vscode/settings.json
create mode 100644 packages/cli-old/template/nextjs-shadcn/README.md
create mode 100644 packages/cli-old/template/nextjs-shadcn/_gitignore
create mode 100644 packages/cli-old/template/nextjs-shadcn/biome.json
create mode 100644 packages/cli-old/template/nextjs-shadcn/components.json
create mode 100644 packages/cli-old/template/nextjs-shadcn/next.config.ts
create mode 100644 packages/cli-old/template/nextjs-shadcn/package.json
create mode 100644 packages/cli-old/template/nextjs-shadcn/postcss.config.mjs
create mode 100644 packages/cli-old/template/nextjs-shadcn/proofkit.json
create mode 100644 packages/cli-old/template/nextjs-shadcn/public/favicon.ico
create mode 100644 packages/cli-old/template/nextjs-shadcn/public/proofkit.png
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/globals.css
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/layout.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/navigation.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppLogo.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/config.ts
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/mode-toggle.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/providers.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/theme-provider.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/button.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/sonner.tsx
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/lib/env.ts
create mode 100644 packages/cli-old/template/nextjs-shadcn/src/lib/utils.ts
create mode 100644 packages/cli-old/template/nextjs-shadcn/tsconfig.json
create mode 100644 packages/cli-old/template/pages/nextjs/blank/page.tsx
create mode 100644 packages/cli-old/template/pages/nextjs/table-edit/actions.ts
create mode 100644 packages/cli-old/template/pages/nextjs/table-edit/page.tsx
create mode 100644 packages/cli-old/template/pages/nextjs/table-edit/schema.ts
create mode 100644 packages/cli-old/template/pages/nextjs/table-edit/table.tsx
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/actions.ts
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/page.tsx
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/query.ts
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/schema.ts
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/table.tsx
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/actions.ts
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/page.tsx
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/query.ts
create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/table.tsx
create mode 100644 packages/cli-old/template/pages/nextjs/table/page.tsx
create mode 100644 packages/cli-old/template/pages/nextjs/table/table.tsx
create mode 100644 packages/cli-old/template/pages/vite-wv/blank/index.tsx
create mode 100644 packages/cli-old/template/pages/vite-wv/table-edit/index.tsx
create mode 100644 packages/cli-old/template/pages/vite-wv/table/index.tsx
create mode 100644 packages/cli-old/template/vite-wv/.claude/launch.json
create mode 100644 packages/cli-old/template/vite-wv/.vscode/settings.json
create mode 100644 packages/cli-old/template/vite-wv/AGENTS.md
create mode 100644 packages/cli-old/template/vite-wv/CLAUDE.md
create mode 100644 packages/cli-old/template/vite-wv/_gitignore
create mode 100644 packages/cli-old/template/vite-wv/components.json
create mode 100644 packages/cli-old/template/vite-wv/index.html
create mode 100644 packages/cli-old/template/vite-wv/package.json
create mode 100644 packages/cli-old/template/vite-wv/proofkit-typegen.config.jsonc
create mode 100644 packages/cli-old/template/vite-wv/proofkit.json
create mode 100644 packages/cli-old/template/vite-wv/scripts/filemaker.js
create mode 100644 packages/cli-old/template/vite-wv/scripts/launch-fm.js
create mode 100644 packages/cli-old/template/vite-wv/scripts/upload.js
create mode 100644 packages/cli-old/template/vite-wv/src/App.tsx
create mode 100644 packages/cli-old/template/vite-wv/src/index.css
create mode 100644 packages/cli-old/template/vite-wv/src/lib/utils.ts
create mode 100644 packages/cli-old/template/vite-wv/src/main.tsx
create mode 100644 packages/cli-old/template/vite-wv/src/router.tsx
create mode 100644 packages/cli-old/template/vite-wv/src/routes/query-demo.tsx
create mode 100644 packages/cli-old/template/vite-wv/tsconfig.json
create mode 100644 packages/cli-old/template/vite-wv/vite.config.ts
rename packages/{cli/tests/browser-apps.test.ts => cli-old/tests/browser-apps.smoke.test.ts} (92%)
create mode 100644 packages/cli-old/tests/cli.test.ts
create mode 100644 packages/cli-old/tests/init-non-interactive-failures.test.ts
create mode 100644 packages/cli-old/tests/init-post-init-generation-errors.test.ts
create mode 100644 packages/cli-old/tests/init-run-init-regression.test.ts
create mode 100644 packages/cli-old/tests/init-scaffold-contract.test.ts
create mode 100644 packages/cli-old/tests/setup.ts
create mode 100644 packages/cli-old/tests/test-utils.ts
create mode 100644 packages/cli-old/tests/webviewer-apps.test.ts
create mode 100644 packages/cli-old/tsconfig.json
create mode 100644 packages/cli-old/tsdown.config.ts
create mode 100644 packages/cli-old/vitest.config.ts
create mode 100644 packages/cli-old/vitest.smoke.config.ts
rename packages/{new => cli}/src/core/context.ts (94%)
rename packages/{new => cli}/src/core/errors.ts (100%)
rename packages/{new => cli}/src/core/executeInitPlan.ts (78%)
rename packages/{new => cli}/src/core/planInit.ts (94%)
rename packages/{new => cli}/src/core/resolveInitRequest.ts (95%)
rename packages/{new => cli}/src/core/types.ts (100%)
rename packages/{new => cli}/src/services/live.ts (100%)
rename packages/{new => cli}/src/utils/browserOpen.ts (100%)
rename packages/{new => cli}/src/utils/http.ts (100%)
rename packages/{new => cli}/src/utils/packageManager.ts (100%)
rename packages/{new => cli}/src/utils/projectFiles.ts (90%)
rename packages/{new => cli}/src/utils/projectName.ts (100%)
rename packages/{new => cli}/src/utils/prompts.ts (100%)
rename packages/{new => cli}/src/utils/versioning.ts (100%)
create mode 100644 packages/cli/tests/browser-apps.smoke.test.ts
rename packages/{new => cli}/tests/default-command.test.ts (91%)
rename packages/{new => cli}/tests/executor.test.ts (100%)
rename packages/{new => cli}/tests/init-fixtures.ts (75%)
create mode 100644 packages/cli/tests/init-non-interactive-failures.test.ts
create mode 100644 packages/cli/tests/init-post-init-generation-errors.test.ts
create mode 100644 packages/cli/tests/init-run-init-regression.test.ts
create mode 100644 packages/cli/tests/init-scaffold-contract.test.ts
rename packages/{new => cli}/tests/integration.test.ts (70%)
rename packages/{new => cli}/tests/planner.test.ts (97%)
rename packages/{new => cli}/tests/project-name.test.ts (100%)
rename packages/{new => cli}/tests/prompts.test.ts (100%)
rename packages/{new => cli}/tests/resolve-init.test.ts (100%)
rename packages/{new => cli}/tests/test-layer.ts (100%)
rename packages/{new/vitest.config.ts => cli/vitest.smoke.config.ts} (84%)
create mode 100644 packages/create-proofkit/tests/index.test.js
create mode 100644 packages/create-proofkit/vitest.config.ts
delete mode 100644 packages/new/package.json
delete mode 100644 packages/new/src/utils/renderTitle.ts
delete mode 100644 packages/new/tests/cli.test.ts
delete mode 100644 packages/new/tsconfig.json
delete mode 100644 packages/new/tsdown.config.ts
diff --git a/.changeset/proofkit-cli-major-migration.md b/.changeset/proofkit-cli-major-migration.md
new file mode 100644
index 00000000..e872015f
--- /dev/null
+++ b/.changeset/proofkit-cli-major-migration.md
@@ -0,0 +1,5 @@
+---
+"@proofkit/cli": major
+---
+
+Rewrite the CLI package for better observability, composability, and error tracing.
diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml
index d9317656..f807bae6 100644
--- a/.github/workflows/continuous-release.yml
+++ b/.github/workflows/continuous-release.yml
@@ -62,7 +62,7 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- - name: Run Unit Tests
+ - name: Run Deterministic Contract Tests
run: pnpm test
build:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6395033d..b57ee110 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -59,9 +59,12 @@ jobs:
doppler configure set project proofkit
doppler configure set config test
- - name: Run Tests
+ - name: Run Deterministic Contract Tests
run: pnpm test
+ - name: Run CLI External Integration Smoke Tests
+ run: doppler run -- pnpm --filter @proofkit/cli test:smoke
+
- name: Run fmodata E2E Tests
run: pnpm --filter @proofkit/fmodata test:e2e
diff --git a/packages/cli-old/.yarnrc.yml b/packages/cli-old/.yarnrc.yml
new file mode 100644
index 00000000..c2e3ce63
--- /dev/null
+++ b/packages/cli-old/.yarnrc.yml
@@ -0,0 +1,5 @@
+packageExtensions:
+ chalk@5.0.1:
+ dependencies:
+ "#ansi-styles": npm:ansi-styles@6.1.0
+ "#supports-color": npm:supports-color@9.2.2
diff --git a/packages/cli-old/CHANGELOG.md b/packages/cli-old/CHANGELOG.md
new file mode 100644
index 00000000..7f74ccb5
--- /dev/null
+++ b/packages/cli-old/CHANGELOG.md
@@ -0,0 +1,285 @@
+# @proofgeist/kit
+
+## 2.0.0-beta.22
+
+### Minor Changes
+
+- 5544f68: - cli: Revamp the WebViewer Vite template and harden `proofkit init` (ignore hidden files, improve non-interactive prompts, stop generating Cursor rules).
+ - cli: Install typegen skills locally when scaffolding projects.
+ - typegen: Add optional `fmHttp` config for using an FM HTTP proxy during metadata fetching.
+ - fmdapi/fmodata/webviewer: Add initial Codex skills for client and integration workflows.
+
+### Patch Changes
+
+- Updated dependencies [5544f68]
+- Updated dependencies [f3980b1]
+- Updated dependencies [8ca7a1e]
+- Updated dependencies [1d4b69d]
+ - @proofkit/typegen@1.1.0-beta.17
+ - @proofkit/fmdapi@5.1.0-beta.2
+
+## 2.0.0-beta.21
+
+### Patch Changes
+
+- Updated dependencies [2df365d]
+ - @proofkit/typegen@1.1.0-beta.16
+
+## 2.0.0-beta.20
+
+### Patch Changes
+
+- @proofkit/typegen@1.1.0-beta.15
+
+## 2.0.0-beta.19
+
+### Patch Changes
+
+- Updated dependencies [4e048d1]
+ - @proofkit/typegen@1.1.0-beta.14
+
+## 2.0.0-beta.18
+
+### Patch Changes
+
+- Updated dependencies [4928637]
+ - @proofkit/typegen@1.1.0-beta.13
+
+## 2.0.0-beta.17
+
+### Patch Changes
+
+- @proofkit/typegen@1.1.0-beta.12
+
+## 2.0.0-beta.16
+
+### Patch Changes
+
+- @proofkit/typegen@1.1.0-beta.11
+
+## 2.0.0-beta.15
+
+### Patch Changes
+
+- @proofkit/typegen@1.1.0-beta.10
+
+## 2.0.0-beta.14
+
+### Patch Changes
+
+- Updated dependencies [eb7d751]
+ - @proofkit/typegen@1.1.0-beta.9
+
+## 2.0.0-beta.13
+
+### Patch Changes
+
+- @proofkit/typegen@1.1.0-beta.8
+
+## 2.0.0-beta.12
+
+### Patch Changes
+
+- Updated dependencies [3b55d14]
+ - @proofkit/typegen@1.1.0-beta.7
+
+## 2.0.0-beta.11
+
+### Patch Changes
+
+- Updated dependencies
+ - @proofkit/typegen@1.1.0-beta.6
+
+## 2.0.0-beta.10
+
+### Patch Changes
+
+- Updated dependencies [ae07372]
+- Updated dependencies [23639ec]
+- Updated dependencies [dfe52a7]
+ - @proofkit/typegen@1.1.0-beta.5
+
+## 2.0.0-beta.9
+
+### Patch Changes
+
+- 863e1e8: Update tooling to Biome
+- Updated dependencies [7dbfd63]
+- Updated dependencies [863e1e8]
+ - @proofkit/typegen@1.1.0-beta.4
+ - @proofkit/fmdapi@5.0.3-beta.1
+
+## 2.0.0-beta.8
+
+### Patch Changes
+
+- @proofkit/typegen@1.1.0-beta.3
+
+## 2.0.0-beta.4
+
+### Patch Changes
+
+- Updated dependencies [4d9d0e9]
+ - @proofkit/typegen@1.0.11-beta.1
+
+## 1.1.8
+
+### Patch Changes
+
+- 00177bf: Guard page add/remove against missing `src/app/navigation.tsx` so WebViewer apps don’t error when updating navigation. This safely no-ops when the navigation file isn’t present.
+- Updated dependencies [7c602a9]
+- Updated dependencies [a29ca94]
+ - @proofkit/typegen@1.0.10
+ - @proofkit/fmdapi@5.0.2
+
+## 1.1.5
+
+### Patch Changes
+
+- Run typegen code directly instead of via execa
+- error trap around formatting
+- Remove shared-utils dep
+
+## 1.1.0
+
+### Minor Changes
+
+- 7429a1e: Add simultaneous support for Shadcn. New projects will have Shadcn initialized automatically, and the upgrade command will offer to automatically add support for Shadcn to an existing ProofKit project.
+
+### Patch Changes
+
+- b483d67: Update formatting after typegen to be more consistent
+- f0ddde2: Upgrade next-safe-action to v8 (and related dependencies)
+- 7c87649: Fix getFieldNamesForSchema function
+
+## 1.0.0
+
+### Major Changes
+
+- c348e37: Support @proofkit namespaced packages
+
+### Patch Changes
+
+- Updated dependencies [16fb8bd]
+- Updated dependencies [16fb8bd]
+- Updated dependencies [16fb8bd]
+ - @proofkit/fmdapi@5.0.0
+
+## 0.3.2
+
+### Patch Changes
+
+- 8986819: Fix: name argument in add command optional
+- 47aad62: Make the auth installer spinner good
+
+## 0.3.1
+
+### Patch Changes
+
+- 467d0f9: Add new menu command to expose all proofkit functions more easily
+- 6da944a: Ensure using authedActionClient in existing actions after adding auth
+- b211fbd: Deploy command: run build on Vercel instead of locally. Use flag --local-build to build locally like before
+- 39648a9: Fix: Webviewer addon installation flow
+- d0627b2: update base package versions
+
+## 0.3.0
+
+### Minor Changes
+
+- 846ae9a: Add new upgrade command to upgrade ProofKit components in an existing project. To start, this command only adds/updates the cursor rules in your project.
+
+### Patch Changes
+
+- e07341a: Always use accessorFn for tables for better type errors
+
+## 0.2.3
+
+### Patch Changes
+
+- 217eb5b: Fixed infinite table queries for other field names
+- 217eb5b: New infinite table editable template
+
+## 0.2.2
+
+### Patch Changes
+
+- ffae753: Better https parsing when prompting for the FileMaker Server URL
+- 415be19: Add options for password strength in fm-addon auth. Default to not check for compromised passwords
+- af5feba: Fix the launch-fm script for web viewer
+
+## 0.2.1
+
+### Patch Changes
+
+- 6e44193: update helper text for npm after adding page
+- 6e44193: additional supression of hydration warning
+- 6e44193: move question about adding data source for new project
+- 183988b: fix import path for reset password helper
+- 6e44193: Make an initial commit when initializing git repo
+- e0682aa: Copy cursor rules.mdc file into the base project.
+
+## 0.2.0
+
+### Minor Changes
+
+- 6073cfe: Allow deploying a demo file to your server instead of having to pick an existing file
+
+### Patch Changes
+
+- d0f5c6e: Fix: post-install template functions not running
+
+## 0.1.2
+
+### Patch Changes
+
+- 92cb423: fix: runtime error due to external shared package
+
+## 0.1.1
+
+### Patch Changes
+
+- f88583c: prompt user to login to Vercel if needed during deploy command
+
+## 0.1.0
+
+### Minor Changes
+
+- c019363: Add Deploy command for Vercel
+
+### Patch Changes
+
+- 0b7bf78: Allow setup without any data sources
+
+## 0.0.15
+
+### Patch Changes
+
+- 1ff4aa7: Hide options for unsupported features in webviewer apps
+- 5cfd0aa: Add infinite table page template
+- 063859a: Added Template: Editable Table
+- de0c2ab: update shebang in index
+- b7ad0cf: Stream output from the typegen command
+
+## 0.0.6
+
+### Patch Changes
+
+- Adding pages
+
+## 0.0.3
+
+### Patch Changes
+
+- add typegen command for fm
+
+## 0.0.2
+
+### Patch Changes
+
+- fix auth in init
+
+## 0.0.2-beta.0
+
+### Patch Changes
+
+- fix auth in init
diff --git a/packages/cli-old/README.md b/packages/cli-old/README.md
new file mode 100644
index 00000000..86380fb3
--- /dev/null
+++ b/packages/cli-old/README.md
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ ProofKit CLI
+
+
+
+ Interactive CLI to manage your TypeScript projects that connect with FileMaker
+
+
+
+ Get started with a new ProofKit project by running pnpm create proofkit
+
+
+View full documentation at [proofkit.dev](https://proofkit.dev)
diff --git a/packages/cli-old/index.d.ts b/packages/cli-old/index.d.ts
new file mode 100644
index 00000000..61865039
--- /dev/null
+++ b/packages/cli-old/index.d.ts
@@ -0,0 +1,19 @@
+export interface RouteLink {
+ label: string;
+ type: "link";
+ href: string;
+ icon?: React.ReactNode;
+ /** If true, the route will only be considered active if the path is exactly this value. */
+ exactMatch?: boolean;
+}
+
+export interface RouteFunction {
+ label: string;
+ type: "function";
+ icon?: React.ReactNode;
+ onClick: () => void;
+ /** If true, the route will only be considered active if the path is exactly this value. */
+ exactMatch?: boolean;
+}
+
+export type ProofKitRoute = RouteLink | RouteFunction;
diff --git a/packages/cli-old/package.json b/packages/cli-old/package.json
new file mode 100644
index 00000000..dfcd3c24
--- /dev/null
+++ b/packages/cli-old/package.json
@@ -0,0 +1,128 @@
+{
+ "name": "@proofkit/cli-old",
+ "version": "2.0.0-beta.22",
+ "private": true,
+ "description": "Create web application with the ProofKit stack",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/proofgeist/proofkit.git",
+ "directory": "packages/cli-old"
+ },
+ "keywords": [
+ "proofkit",
+ "filemaker",
+ "ottomatic",
+ "proofgeist",
+ "next.js",
+ "typescript"
+ ],
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./index.d.ts",
+ "import": "./dist/index.js"
+ }
+ },
+ "files": [
+ "dist",
+ "template",
+ "README.md",
+ "index.d.ts",
+ "LICENSE",
+ "CHANGELOG.md",
+ "package.json"
+ ],
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0"
+ },
+ "scripts": {
+ "typecheck": "tsc",
+ "build": "NODE_ENV=production tsdown && publint --strict",
+ "prepublishOnly": "pnpm build",
+ "dev": "tsdown --watch",
+ "clean": "rm -rf dist .turbo node_modules",
+ "start": "node dist/index.js",
+ "lint": "biome check . --write",
+ "lint:summary": "biome check . --reporter=summary",
+ "release": "changeset version",
+ "test": "pnpm test:contract",
+ "test:contract": "vitest run --config vitest.config.ts",
+ "test:smoke": "vitest run --config vitest.smoke.config.ts"
+ },
+ "dependencies": {
+ "@better-fetch/fetch": "1.1.17",
+ "@clack/core": "^0.3.5",
+ "@clack/prompts": "^0.11.0",
+ "@inquirer/prompts": "^8.3.2",
+ "@proofkit/fmdapi": "workspace:*",
+ "@proofkit/typegen": "workspace:*",
+ "@types/glob": "^8.1.0",
+ "axios": "^1.13.2",
+ "chalk": "5.4.1",
+ "commander": "^14.0.2",
+ "dotenv": "^16.6.1",
+ "es-toolkit": "^1.43.0",
+ "execa": "^9.6.1",
+ "fast-glob": "^3.3.3",
+ "fs-extra": "^11.3.3",
+ "glob": "^11.1.0",
+ "gradient-string": "^2.0.2",
+ "handlebars": "^4.7.8",
+ "jiti": "^1.21.7",
+ "jsonc-parser": "^3.3.1",
+ "open": "^10.2.0",
+ "ora": "6.3.1",
+ "randomstring": "^1.3.1",
+ "semver": "^7.7.3",
+ "shadcn": "^2.10.0",
+ "sort-package-json": "^2.15.1",
+ "ts-morph": "^26.0.0"
+ },
+ "devDependencies": {
+ "@auth/drizzle-adapter": "^1.11.1",
+ "@auth/prisma-adapter": "^1.6.0",
+ "@biomejs/biome": "2.3.11",
+ "@libsql/client": "^0.6.2",
+ "@planetscale/database": "^1.19.0",
+ "@prisma/adapter-planetscale": "^5.22.0",
+ "@prisma/client": "^5.22.0",
+ "@proofkit/registry": "workspace:*",
+ "@rollup/plugin-replace": "^6.0.3",
+ "@t3-oss/env-nextjs": "^0.10.1",
+ "@tanstack/react-query": "^5.90.16",
+ "@trpc/client": "11.0.0-rc.441",
+ "@trpc/next": "11.0.0-rc.441",
+ "@trpc/react-query": "11.0.0-rc.441",
+ "@trpc/server": "11.0.0-rc.441",
+ "@types/axios": "^0.14.4",
+ "@types/fs-extra": "^11.0.4",
+ "@types/gradient-string": "^1.1.6",
+ "@types/node": "^22.19.5",
+ "@types/randomstring": "^1.3.0",
+ "@types/react": "19.2.7",
+ "@types/semver": "^7.7.1",
+ "@vitest/coverage-v8": "^2.1.9",
+ "drizzle-kit": "^0.21.4",
+ "drizzle-orm": "^0.30.10",
+ "mysql2": "^3.16.0",
+ "next": "16.1.1",
+ "next-auth": "^4.24.13",
+ "postgres": "^3.4.8",
+ "prisma": "^5.22.0",
+ "publint": "^0.3.16",
+ "react": "19.2.3",
+ "react-dom": "19.2.3",
+ "superjson": "^2.2.6",
+ "tailwindcss": "^4.1.18",
+ "tsdown": "^0.14.2",
+ "type-fest": "^3.13.1",
+ "typescript": "^5.9.3",
+ "ultracite": "7.0.8",
+ "vitest": "^4.0.17",
+ "zod": "^4.3.5"
+ },
+ "publishConfig": {
+ "access": "restricted"
+ }
+}
diff --git a/packages/cli/proofkit-cli-1.1.8.tgz b/packages/cli-old/proofkit-cli-1.1.8.tgz
similarity index 100%
rename from packages/cli/proofkit-cli-1.1.8.tgz
rename to packages/cli-old/proofkit-cli-1.1.8.tgz
diff --git a/packages/cli-old/src/cli/add/auth.ts b/packages/cli-old/src/cli/add/auth.ts
new file mode 100644
index 00000000..cab277f9
--- /dev/null
+++ b/packages/cli-old/src/cli/add/auth.ts
@@ -0,0 +1,110 @@
+import chalk from "chalk";
+import { Command } from "commander";
+import { z } from "zod/v4";
+import { cancel, select } from "~/cli/prompts.js";
+
+import { addAuth } from "~/generators/auth.js";
+import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js";
+import { initProgramState, isNonInteractiveMode, state } from "~/state.js";
+import { getSettings } from "~/utils/parseSettings.js";
+import { abortIfCancel } from "../utils.js";
+
+export async function runAddAuthAction() {
+ const settings = getSettings();
+ if (settings.appType !== "browser") {
+ return cancel("Auth is not supported for your app type.");
+ }
+ if (settings.ui === "shadcn") {
+ return cancel("Adding auth is not yet supported for shadcn-based projects.");
+ }
+
+ const authType =
+ state.authType ??
+ abortIfCancel(
+ await select({
+ message: "What auth provider do you want to use?",
+ options: [
+ {
+ value: "fmaddon",
+ label: "FM Add-on Auth",
+ hint: "Self-hosted auth with email/password",
+ },
+ {
+ value: "clerk",
+ label: "Clerk",
+ hint: "Hosted auth service with many providers",
+ },
+ ],
+ }),
+ );
+
+ const type = z.enum(["clerk", "fmaddon"]).parse(authType);
+ state.authType = type;
+
+ if (type === "fmaddon") {
+ const emailProviderAnswer =
+ state.emailProvider ??
+ (isNonInteractiveMode() ? "none" : undefined) ??
+ abortIfCancel(
+ await select({
+ message: `What email provider do you want to use?\n${chalk.dim(
+ "Used to send email verification codes. If you skip this, the codes will be displayed here in your terminal.",
+ )}`,
+ options: [
+ {
+ label: "Resend",
+ value: "resend",
+ hint: "Great dev experience",
+ },
+ {
+ label: "Plunk",
+ value: "plunk",
+ hint: "Cheapest for <20k emails/mo, self-hostable",
+ },
+ { label: "Other / I'll do it myself later", value: "none" },
+ ],
+ }),
+ );
+
+ const emailProvider = z.enum(["plunk", "resend", "none"]).parse(emailProviderAnswer);
+
+ state.emailProvider = emailProvider;
+
+ await addAuth({
+ options: {
+ type,
+ emailProvider: emailProvider === "none" ? undefined : emailProvider,
+ },
+ });
+ } else {
+ await addAuth({ options: { type } });
+ }
+}
+
+export const makeAddAuthCommand = () => {
+ const addAuthCommand = new Command("auth")
+ .description("Add authentication to your project")
+ .option("--authType ", "Type of auth provider to use")
+ .option("--emailProvider ", "Email provider to use (only for FM Add-on Auth)")
+ .option("--apiKey ", "API key to use for the email provider (only for FM Add-on Auth)")
+ .addOption(ciOption)
+ .addOption(nonInteractiveOption)
+ .addOption(debugOption)
+
+ .action(async () => {
+ const settings = getSettings();
+ if (settings.ui === "shadcn") {
+ throw new Error("Shadcn projects should add auth using the template registry");
+ }
+ if (settings.auth.type !== "none") {
+ throw new Error("Auth already exists");
+ }
+ await runAddAuthAction();
+ });
+
+ addAuthCommand.hook("preAction", (thisCommand) => {
+ initProgramState(thisCommand.opts());
+ });
+
+ return addAuthCommand;
+};
diff --git a/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts b/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts
new file mode 100644
index 00000000..61bcebe4
--- /dev/null
+++ b/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts
@@ -0,0 +1,96 @@
+import { createDataAPIKeyWithCredentials, getDeploymentStatus, startDeployment } from "~/cli/ottofms.js";
+import * as p from "~/cli/prompts.js";
+
+export const filename = "ProofKitDemo.fmp12";
+
+export async function deployDemoFile({
+ url,
+ token,
+ operation,
+}: {
+ url: URL;
+ token: string;
+ operation: "install" | "replace";
+}): Promise<{ apiKey: string }> {
+ const deploymentJSON = {
+ scheduled: false,
+ label: "Install ProofKit Demo",
+ deployments: [
+ {
+ name: "Install ProofKit Demo",
+ source: {
+ type: "url",
+ url: "https://proofkit.dev/proofkit-demo/manifest.json",
+ },
+ fileOperations: [
+ {
+ target: {
+ fileName: filename,
+ },
+ operation,
+ source: {
+ fileName: "ProofKitDemo.fmp12",
+ },
+ location: {
+ folder: "default",
+ subFolder: "",
+ },
+ },
+ ],
+ concurrency: 1,
+ options: {
+ closeFilesAfterBuild: false,
+ keepFilesClosedAfterComplete: false,
+ transferContainerData: false,
+ },
+ },
+ ],
+ abortRemaining: false,
+ };
+
+ const spinner = p.spinner();
+ spinner.start("Deploying ProofKit Demo file...");
+
+ const {
+ response: { subDeploymentIds },
+ } = await startDeployment({
+ payload: deploymentJSON,
+ url,
+ token,
+ });
+
+ const deploymentId = subDeploymentIds[0];
+ if (!deploymentId) {
+ throw new Error("No deployment ID returned from the server");
+ }
+
+ while (true) {
+ // wait 2.5 seconds, then poll the status again
+ await new Promise((resolve) => setTimeout(resolve, 2500));
+
+ const {
+ response: { status, running },
+ } = await getDeploymentStatus({
+ url,
+ token,
+ deploymentId,
+ });
+ if (!running) {
+ if (status !== "complete") {
+ throw new Error("Deployment didn't complete");
+ }
+ break;
+ }
+ }
+
+ const { apiKey } = await createDataAPIKeyWithCredentials({
+ filename,
+ username: "admin",
+ password: "admin",
+ url,
+ });
+
+ spinner.stop();
+
+ return { apiKey };
+}
diff --git a/packages/cli-old/src/cli/add/data-source/filemaker.ts b/packages/cli-old/src/cli/add/data-source/filemaker.ts
new file mode 100644
index 00000000..1546f2a4
--- /dev/null
+++ b/packages/cli-old/src/cli/add/data-source/filemaker.ts
@@ -0,0 +1,441 @@
+import chalk from "chalk";
+import { SemVer } from "semver";
+import type { z } from "zod/v4";
+import { createDataAPIKey, getOttoFMSToken, listAPIKeys, listFiles } from "~/cli/ottofms.js";
+import * as p from "~/cli/prompts.js";
+import { abortIfCancel } from "~/cli/utils.js";
+import { addLayout, addToFmschemaConfig, ensureWebviewerFmHttpConfig } from "~/generators/fmdapi.js";
+import { getFmHttpStatus } from "~/helpers/fmHttp.js";
+import { fetchServerVersions } from "~/helpers/version-fetcher.js";
+import { isNonInteractiveMode } from "~/state.js";
+import { addPackageDependency } from "~/utils/addPackageDependency.js";
+import { addToEnv } from "~/utils/addToEnvs.js";
+import { type dataSourceSchema, getSettings, setSettings } from "~/utils/parseSettings.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+import { validateAppName } from "~/utils/validateAppName.js";
+import { runAddSchemaAction } from "../fmschema.js";
+import { deployDemoFile, filename } from "./deploy-demo-file.js";
+
+export async function promptForFileMakerDataSource({
+ projectDir,
+ ...opts
+}: {
+ projectDir: string;
+ name?: string;
+ server?: string;
+ adminApiKey?: string;
+ fileName?: string;
+ dataApiKey?: string;
+ layoutName?: string;
+ schemaName?: string;
+}) {
+ const settings = getSettings();
+
+ if (settings.appType === "webviewer") {
+ const fmHttpStatus = await getFmHttpStatus();
+ const connectedFileName = fmHttpStatus.connectedFiles[0];
+ const localDataSourceName = opts.name ?? "filemaker";
+
+ if (!opts.server && fmHttpStatus.healthy && connectedFileName) {
+ addPackageDependency({
+ projectDir,
+ dependencies: ["@proofkit/fmdapi"],
+ devMode: false,
+ });
+
+ await ensureWebviewerFmHttpConfig({
+ projectDir,
+ connectedFileName,
+ dataSourceName: localDataSourceName,
+ baseUrl: fmHttpStatus.baseUrl,
+ });
+
+ // Persist the datasource in project settings
+ const newDataSource: z.infer = {
+ type: "fm",
+ name: localDataSourceName,
+ envNames:
+ localDataSourceName === "filemaker"
+ ? {
+ database: "FM_DATABASE",
+ server: "FM_SERVER",
+ apiKey: "OTTO_API_KEY",
+ }
+ : {
+ database: `${localDataSourceName.toUpperCase()}_FM_DATABASE`,
+ server: `${localDataSourceName.toUpperCase()}_FM_SERVER`,
+ apiKey: `${localDataSourceName.toUpperCase()}_OTTO_API_KEY`,
+ },
+ };
+ settings.dataSources.push(newDataSource);
+ setSettings(settings);
+
+ if (opts.layoutName && opts.schemaName) {
+ await addLayout({
+ projectDir,
+ dataSourceName: localDataSourceName,
+ schemas: [
+ {
+ layoutName: opts.layoutName,
+ schemaName: opts.schemaName,
+ valueLists: "allowEmpty",
+ },
+ ],
+ });
+ } else if (opts.layoutName || opts.schemaName) {
+ throw new Error("Both --layoutName and --schemaName must be provided together.");
+ } else {
+ p.note(
+ `Detected local FM HTTP at ${fmHttpStatus.baseUrl} with connected file "${connectedFileName}". Edit ${chalk.cyan(
+ "proofkit-typegen.config.jsonc",
+ )} to add layouts, then run ${chalk.cyan("pnpm typegen")} or ${chalk.cyan("pnpm typegen:ui")}.`,
+ "Local FileMaker detected",
+ );
+ }
+
+ return;
+ }
+
+ if (!opts.server && isNonInteractiveMode()) {
+ throw new Error(
+ "No local FM HTTP connection was detected and no FileMaker server was provided. Start the local FM HTTP proxy with a connected file or rerun with --server.",
+ );
+ }
+
+ if (!opts.server) {
+ const fallbackAction = abortIfCancel(
+ await p.select({
+ message:
+ "Local FM HTTP was not detected. Do you want to continue with hosted FileMaker server setup or skip for now?",
+ options: [
+ {
+ label: "Continue with hosted setup",
+ value: "hosted",
+ },
+ {
+ label: "Skip for now",
+ value: "skip",
+ },
+ ],
+ }),
+ );
+
+ if (fallbackAction === "skip") {
+ p.note(
+ `You can come back later with ${chalk.cyan("proofkit add data")} after starting FM HTTP locally or when you have a hosted server ready.`,
+ );
+ return;
+ }
+ }
+ }
+
+ const existingFmDataSourceNames = settings.dataSources.filter((ds) => ds.type === "fm").map((ds) => ds.name);
+
+ const server = await getValidFileMakerServerUrl(opts.server);
+
+ const canDoBrowserLogin = server.ottoVersion && server.ottoVersion.compare(new SemVer("4.7.0")) > 0;
+
+ if (!(canDoBrowserLogin || opts.adminApiKey)) {
+ return p.cancel(
+ "OttoFMS 4.7.0 or later is required to auto-login with this CLI. Please install/upgrade OttoFMS on your server, or pass an Admin API key with the --adminApiKey flag then try again",
+ );
+ }
+
+ const token = opts.adminApiKey || (await getOttoFMSToken({ url: server.url })).token;
+
+ const fileList = await listFiles({ url: server.url, token });
+ const demoFileExists = fileList.map((f) => f.filename.replace(".fmp12", "")).includes(filename.replace(".fmp12", ""));
+ let fmFile = opts.fileName;
+ while (true) {
+ fmFile =
+ opts.fileName ||
+ abortIfCancel(
+ await p.searchSelect({
+ message: `Which file would you like to connect to? ${chalk.dim("(TIP: Select the file where your data is stored)")}`,
+ searchLabel: "Search files",
+ emptyMessage: "No matching files found.",
+ options: [
+ {
+ value: "$deployDemoFile",
+ label: "Deploy NEW ProofKit Demo File",
+ hint: "Use OttoFMS to deploy a new file for testing",
+ keywords: ["demo", "proofkit"],
+ },
+ ...fileList
+ .sort((a, b) => a.filename.localeCompare(b.filename))
+ .map((file) => ({
+ value: file.filename,
+ label: file.filename,
+ hint: file.status,
+ keywords: [file.filename],
+ })),
+ ],
+ }),
+ );
+
+ if (fmFile !== "$deployDemoFile") {
+ break;
+ }
+
+ if (demoFileExists) {
+ const replace = abortIfCancel(
+ await p.confirm({
+ message: "The demo file already exists, do you want to replace it with a fresh copy?",
+ active: "Yes, replace",
+ inactive: "No, select another file",
+ initialValue: false,
+ }),
+ );
+ if (replace) {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ if (!fmFile) {
+ throw new Error("No file selected");
+ }
+
+ let dataApiKey = opts.dataApiKey;
+ if (fmFile === "$deployDemoFile") {
+ const { apiKey } = await deployDemoFile({
+ url: server.url,
+ token,
+ operation: demoFileExists ? "replace" : "install",
+ });
+ dataApiKey = apiKey;
+ fmFile = filename;
+ opts.layoutName = opts.layoutName ?? "API_Contacts";
+ opts.schemaName = opts.schemaName ?? "Contacts";
+ } else {
+ const allApiKeys = await listAPIKeys({ url: server.url, token });
+ const thisFileApiKeys = allApiKeys.filter((key) => key.database === fmFile);
+
+ if (!dataApiKey && thisFileApiKeys.length > 0) {
+ const selectedKey = abortIfCancel(
+ await p.searchSelect({
+ message: `Which OttoFMS Data API key would you like to use? ${chalk.dim(`(This determines the access that you'll have to the data in this file)`)}`,
+ searchLabel: "Search API keys",
+ emptyMessage: "No matching API keys found.",
+ options: [
+ ...thisFileApiKeys.map((key) => ({
+ value: key.key,
+ label: `${chalk.bold(key.label)} - ${key.user}`,
+ hint: `${key.key.slice(0, 5)}...${key.key.slice(-4)}`,
+ keywords: [key.label, key.user, key.database],
+ })),
+ {
+ value: "create",
+ label: "Create a new API key",
+ hint: "Requires FileMaker credentials for this file",
+ keywords: ["create", "new"],
+ },
+ ],
+ }),
+ );
+ if (typeof selectedKey !== "string") {
+ throw new Error("Invalid key");
+ }
+ if (selectedKey !== "create") {
+ dataApiKey = selectedKey;
+ }
+ }
+
+ if (!dataApiKey) {
+ // data api was not provided, prompt to create a new one
+ const resp = await createDataAPIKey({
+ filename: fmFile,
+ url: server.url,
+ });
+ dataApiKey = resp.apiKey;
+ }
+ }
+ if (!dataApiKey) {
+ throw new Error("No API key");
+ }
+
+ const name =
+ existingFmDataSourceNames.length === 0
+ ? "filemaker"
+ : (opts.name ??
+ abortIfCancel(
+ await p.text({
+ message: "What do you want to call this data source?",
+ validate: (value) => {
+ if (value === "filemaker") {
+ return "That name is reserved";
+ }
+
+ // require name to be unique
+ if (existingFmDataSourceNames?.includes(value)) {
+ return "That name is already in use in this project, pick something unique";
+ }
+
+ // require name to be alphanumeric, lowercase, etc
+ return validateAppName(value);
+ },
+ }),
+ ));
+
+ const newDataSource: z.infer = {
+ type: "fm",
+ name,
+ envNames:
+ name === "filemaker"
+ ? {
+ database: "FM_DATABASE",
+ server: "FM_SERVER",
+ apiKey: "OTTO_API_KEY",
+ }
+ : {
+ database: `${name.toUpperCase()}_FM_DATABASE`,
+ server: `${name.toUpperCase()}_FM_SERVER`,
+ apiKey: `${name.toUpperCase()}_OTTO_API_KEY`,
+ },
+ };
+
+ const project = getNewProject(projectDir);
+
+ const schemaFile = await addToEnv({
+ projectDir,
+ project,
+ envs: [
+ {
+ name: newDataSource.envNames.database,
+ zodValue: `z.string().endsWith(".fmp12")`,
+ defaultValue: fmFile,
+ type: "server",
+ },
+ {
+ name: newDataSource.envNames.server,
+ zodValue: "z.string().url()",
+ type: "server",
+ defaultValue: server.url.origin,
+ },
+ {
+ name: newDataSource.envNames.apiKey,
+ zodValue: `z.string().startsWith("dk_") as z.ZodType`,
+ type: "server",
+ defaultValue: dataApiKey,
+ },
+ ],
+ });
+
+ const fmdapiImport = schemaFile.getImportDeclaration((imp) => imp.getModuleSpecifierValue() === "@proofkit/fmdapi");
+ if (fmdapiImport) {
+ fmdapiImport
+ .getNamedImports()
+ .find((imp) => imp.getName() === "OttoAPIKey")
+ ?.remove();
+ fmdapiImport.addNamedImport({ name: "OttoAPIKey", isTypeOnly: true });
+ } else {
+ schemaFile.addImportDeclaration({
+ namedImports: [{ name: "OttoAPIKey", isTypeOnly: true }],
+ moduleSpecifier: "@proofkit/fmdapi",
+ });
+ }
+
+ addPackageDependency({
+ projectDir,
+ dependencies: ["@proofkit/fmdapi"],
+ devMode: false,
+ });
+
+ settings.dataSources.push(newDataSource);
+ setSettings(settings);
+
+ addToFmschemaConfig({
+ dataSourceName: name,
+ envNames: name === "filemaker" ? undefined : newDataSource.envNames,
+ });
+
+ await formatAndSaveSourceFiles(project);
+
+ // now prompt for layout
+ await runAddSchemaAction({
+ settings,
+ sourceName: name,
+ projectDir,
+ layoutName: opts.layoutName,
+ schemaName: opts.schemaName,
+ valueLists: "allowEmpty",
+ });
+}
+
+async function getValidFileMakerServerUrl(defaultServerUrl?: string | undefined): Promise<{
+ url: URL;
+ fmsVersion: SemVer;
+ ottoVersion: SemVer | null;
+}> {
+ const spinner = p.spinner();
+ let url: URL | null = null;
+ let fmsVersion: SemVer | null = null;
+ let ottoVersion: SemVer | null = null;
+ let serverUrlToUse = defaultServerUrl;
+
+ while (fmsVersion === null) {
+ const serverUrl =
+ serverUrlToUse ??
+ abortIfCancel(
+ await p.text({
+ message: `What is the URL of your FileMaker Server?\n${chalk.cyan("TIP: You can copy any valid path on the server and paste it here.")}`,
+ validate: (value) => {
+ try {
+ // try to make sure the url is https
+ let normalizedValue = value;
+ if (!normalizedValue.startsWith("https://")) {
+ if (normalizedValue.startsWith("http://")) {
+ normalizedValue = normalizedValue.replace("http://", "https://");
+ } else {
+ normalizedValue = `https://${normalizedValue}`;
+ }
+ }
+
+ // try to make sure the url is valid
+ new URL(normalizedValue);
+ return;
+ } catch {
+ return "Please enter a valid URL";
+ }
+ },
+ }),
+ );
+
+ try {
+ url = new URL(serverUrl);
+ } catch {
+ p.log.error(`Invalid URL: ${serverUrl.toString()}`);
+ continue;
+ }
+
+ spinner.start("Validating Server URL...");
+
+ // check for FileMaker and Otto versions
+ const { fmsInfo, ottoInfo } = await fetchServerVersions({
+ url: url.origin,
+ });
+
+ spinner.stop();
+
+ const fmsVersionString = fmsInfo.ServerVersion.split(" ")[0];
+ if (!fmsVersionString) {
+ p.log.error("Unable to parse FileMaker Server version");
+ serverUrlToUse = undefined;
+ continue;
+ }
+ fmsVersion = new SemVer(fmsVersionString);
+ ottoVersion = ottoInfo?.Otto.version ? new SemVer(ottoInfo.Otto.version) : null;
+ serverUrlToUse = undefined;
+ }
+
+ if (url === null) {
+ throw new Error("Unable to get FileMaker Server URL");
+ }
+
+ p.note(`🎉 FileMaker Server version ${fmsVersion} detected \n
+ ${ottoVersion ? `🎉 OttoFMS version ${ottoVersion} detected` : "❌ OttoFMS not detected"}`);
+
+ return { url, ottoVersion, fmsVersion };
+}
diff --git a/packages/cli-old/src/cli/add/data-source/index.ts b/packages/cli-old/src/cli/add/data-source/index.ts
new file mode 100644
index 00000000..6d73789b
--- /dev/null
+++ b/packages/cli-old/src/cli/add/data-source/index.ts
@@ -0,0 +1,46 @@
+import { Command } from "commander";
+import { z } from "zod/v4";
+import * as p from "~/cli/prompts.js";
+
+import { ensureProofKitProject } from "~/cli/utils.js";
+import { ciOption, nonInteractiveOption } from "~/globalOptions.js";
+import { initProgramState } from "~/state.js";
+import { promptForFileMakerDataSource } from "./filemaker.js";
+
+const dataSourceType = z.enum(["fm", "supabase"]);
+export const runAddDataSourceCommand = async () => {
+ const dataSource = dataSourceType.parse(
+ await p.select({
+ message: "Which data souce do you want to add?",
+ options: [
+ { label: "FileMaker", value: "fm" },
+ { label: "Supabase", value: "supabase" },
+ ],
+ }),
+ );
+
+ if (dataSource === "supabase") {
+ throw new Error("Not implemented");
+ }
+ if (dataSource === "fm") {
+ await promptForFileMakerDataSource({ projectDir: process.cwd() });
+ } else {
+ throw new Error("Invalid data source");
+ }
+};
+
+export const makeAddDataSourceCommand = () => {
+ const addDataSourceCommand = new Command("data");
+ addDataSourceCommand.description("Add a new data source to your project");
+ addDataSourceCommand.addOption(ciOption);
+ addDataSourceCommand.addOption(nonInteractiveOption);
+
+ addDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => {
+ initProgramState(actionCommand.opts());
+ const settings = ensureProofKitProject({ commandName: "add" });
+ actionCommand.setOptionValue("settings", settings);
+ });
+
+ // addDataSourceCommand.action();
+ return addDataSourceCommand;
+};
diff --git a/packages/cli-old/src/cli/add/fmschema.ts b/packages/cli-old/src/cli/add/fmschema.ts
new file mode 100644
index 00000000..b63dc427
--- /dev/null
+++ b/packages/cli-old/src/cli/add/fmschema.ts
@@ -0,0 +1,216 @@
+import path from "node:path";
+import type { OttoAPIKey } from "@proofkit/fmdapi";
+import type { ValueListsOptions } from "@proofkit/typegen/config";
+import chalk from "chalk";
+import { Command } from "commander";
+import dotenv from "dotenv";
+import { z } from "zod/v4";
+import * as p from "~/cli/prompts.js";
+import { addLayout, getExistingSchemas } from "~/generators/fmdapi.js";
+import { state } from "~/state.js";
+import { getSettings, type Settings } from "~/utils/parseSettings.js";
+import { commonFileMakerLayoutPrefixes, getLayouts } from "../fmdapi.js";
+import { abortIfCancel } from "../utils.js";
+
+// Regex to validate JavaScript variable names
+const VALID_JS_VARIABLE_NAME = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
+
+export const runAddSchemaAction = async (opts?: {
+ projectDir?: string;
+ settings: Settings;
+ sourceName?: string;
+ layoutName?: string;
+ schemaName?: string;
+ valueLists?: ValueListsOptions;
+}) => {
+ const settings = getSettings();
+ const projectDir = state.projectDir;
+ let sourceName = opts?.sourceName;
+ if (sourceName) {
+ sourceName = opts?.sourceName;
+ } else if (settings.dataSources.filter((s) => s.type === "fm").length > 1) {
+ // if there is more than one fm data source, we need to prompt for which one to add the layout to
+ const dataSourceName = await p.select({
+ message: "Which FileMaker data source do you want to add a layout to?",
+ options: settings.dataSources.filter((s) => s.type === "fm").map((s) => ({ label: s.name, value: s.name })),
+ });
+ if (p.isCancel(dataSourceName)) {
+ p.cancel();
+ process.exit(0);
+ }
+ sourceName = z.string().parse(dataSourceName);
+ }
+
+ if (!sourceName) {
+ sourceName = "filemaker";
+ }
+
+ const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === sourceName);
+ if (!dataSource) {
+ throw new Error(`FileMaker data source ${sourceName} not found in your ProofKit config`);
+ }
+
+ const spinner = p.spinner();
+ spinner.start("Loading layouts from your FileMaker file...");
+ if (settings.envFile) {
+ dotenv.config({
+ path: path.join(projectDir, settings.envFile),
+ });
+ }
+
+ const dataApiKey = process.env[dataSource.envNames.apiKey];
+ const fmFile = process.env[dataSource.envNames.database];
+ const server = process.env[dataSource.envNames.server];
+
+ if (!(dataApiKey && fmFile && server)) {
+ spinner.stop("Failed to load layouts");
+ p.cancel("Missing required environment variables. Please check your .env file.");
+ process.exit(1);
+ }
+
+ // Validate API key format
+ if (!(dataApiKey.startsWith("KEY_") || dataApiKey.startsWith("dk_"))) {
+ spinner.stop("Failed to load layouts");
+ p.cancel("Invalid API key format. API key must start with 'KEY_' or 'dk_'.");
+ process.exit(1);
+ }
+
+ // Type assertion after validation
+ const validatedApiKey: OttoAPIKey = dataApiKey as OttoAPIKey;
+
+ const layouts = await getLayouts({
+ dataApiKey: validatedApiKey,
+ fmFile,
+ server,
+ });
+
+ const existingConfigResults = getExistingSchemas({
+ projectDir,
+ dataSourceName: sourceName,
+ });
+
+ const existingLayouts = existingConfigResults.map((s) => s.layout).filter(Boolean);
+
+ const existingSchemas = existingConfigResults.map((s) => s.schemaName).filter(Boolean);
+
+ spinner.stop("Loaded layouts from your FileMaker file");
+
+ if (existingLayouts.length > 0) {
+ p.note(existingLayouts.join("\n"), "Detected existing layouts in your project");
+ }
+
+ // list other common layout names to exclude
+ existingLayouts.push("-");
+
+ let passedInLayoutName: string | undefined = opts?.layoutName;
+ if (passedInLayoutName === "" || !layouts.includes(passedInLayoutName ?? "")) {
+ passedInLayoutName = undefined;
+ }
+
+ const selectedLayout =
+ passedInLayoutName ??
+ abortIfCancel(
+ await p.searchSelect({
+ message: "Select a new layout to read data from",
+ searchLabel: "Search layouts",
+ emptyMessage: "No matching layouts found.",
+ options: layouts
+ .filter((layout) => !existingLayouts.includes(layout))
+ .map((layout) => ({
+ label: layout,
+ value: layout,
+ keywords: [layout],
+ })),
+ }),
+ );
+
+ const defaultSchemaName = getDefaultSchemaName(selectedLayout);
+ const schemaName =
+ opts?.schemaName ||
+ abortIfCancel(
+ await p.text({
+ message: `Enter a friendly name for the new schema.\n${chalk.dim("This will the name by which you refer to this layout in your codebase")}`,
+ // initialValue: selectedLayout,
+ defaultValue: defaultSchemaName,
+ placeholder: defaultSchemaName,
+ validate: (input) => {
+ if (input === "") {
+ return; // allow empty input for the default value
+ }
+ // ensure the input is a valid JS variable name
+ if (!VALID_JS_VARIABLE_NAME.test(input)) {
+ return "Name must consist of only alphanumeric characters, '_', and must not start with a number";
+ }
+ if (existingSchemas.includes(input)) {
+ return "Schema name must be unique";
+ }
+ return;
+ },
+ }),
+ ).toString();
+
+ const valueLists =
+ opts?.valueLists ??
+ ((await p.select({
+ message: `Should we use value lists on this layout?\n${chalk.dim(
+ "This will allow fields that contain a value list to be auto-completed in typescript and also validated to prevent incorrect values",
+ )}`,
+ options: [
+ {
+ label: "Yes, but allow empty fields",
+ value: "allowEmpty",
+ hint: "Empty fields or values that don't match the value list will be converted to an empty string",
+ },
+ {
+ label: "Yes; empty values should fail validation",
+ value: "strict",
+ hint: "Empty fields or values that don't match the value list will cause validation to fail",
+ },
+ {
+ label: "No, ignore value lists",
+ value: "ignore",
+ hint: "Fields will just be typed as strings",
+ },
+ ],
+ })) as ValueListsOptions);
+
+ const valueListsValidated = z.enum(["ignore", "allowEmpty", "strict"]).catch("ignore").parse(valueLists);
+
+ await addLayout({
+ runCodegen: true,
+ projectDir,
+ dataSourceName: sourceName,
+ schemas: [
+ {
+ layoutName: selectedLayout,
+ schemaName,
+ valueLists: valueListsValidated,
+ },
+ ],
+ });
+
+ p.outro(`Layout "${selectedLayout}" added to your project as "${schemaName}"`);
+};
+
+export const makeAddSchemaCommand = () => {
+ const addSchemaCommand = new Command("layout")
+ .alias("schema")
+ .description("Add a new layout to your fmschema file")
+ .action(async (opts: { settings: Settings }) => {
+ const settings = opts.settings;
+
+ await runAddSchemaAction({ settings });
+ });
+
+ return addSchemaCommand;
+};
+
+function getDefaultSchemaName(layout: string) {
+ let schemaName = layout.replace(/[-\s]/g, "_");
+ for (const prefix of commonFileMakerLayoutPrefixes) {
+ if (schemaName.startsWith(prefix)) {
+ schemaName = schemaName.replace(prefix, "");
+ }
+ }
+ return schemaName;
+}
diff --git a/packages/cli-old/src/cli/add/index.ts b/packages/cli-old/src/cli/add/index.ts
new file mode 100644
index 00000000..b3e0d543
--- /dev/null
+++ b/packages/cli-old/src/cli/add/index.ts
@@ -0,0 +1,190 @@
+import type { RegistryIndex } from "@proofkit/registry";
+import { Command } from "commander";
+import { capitalize, groupBy, uniq } from "es-toolkit";
+import ora from "ora";
+import { select } from "~/cli/prompts.js";
+import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js";
+import { initProgramState, state } from "~/state.js";
+import { logger } from "~/utils/logger.js";
+import { getSettings, type Settings } from "~/utils/parseSettings.js";
+import { runAddReactEmailCommand } from "../react-email.js";
+import { runAddTanstackQueryCommand } from "../tanstack-query.js";
+import { abortIfCancel, ensureProofKitProject } from "../utils.js";
+import { makeAddAuthCommand, runAddAuthAction } from "./auth.js";
+import { makeAddDataSourceCommand, runAddDataSourceCommand } from "./data-source/index.js";
+import { makeAddSchemaCommand, runAddSchemaAction } from "./fmschema.js";
+import { makeAddPageCommand, runAddPageAction } from "./page/index.js";
+import { installFromRegistry } from "./registry/install.js";
+import { listItems } from "./registry/listItems.js";
+import { preflightAddCommand } from "./registry/preflight.js";
+
+const runAddFromRegistry = async (_options?: { noInstall?: boolean }) => {
+ const settings = getSettings();
+
+ const spinner = ora("Loading available components...").start();
+ let items: RegistryIndex;
+ try {
+ items = await listItems();
+ } catch (error) {
+ spinner.fail("Failed to load registry components");
+ logger.error(error);
+ return;
+ }
+
+ const itemsNotInstalled = items.filter((item) => !settings.registryTemplates.includes(item.name));
+
+ const groupedByCategory = groupBy(itemsNotInstalled, (item) => item.category);
+ const categories = uniq(itemsNotInstalled.map((item) => item.category));
+
+ spinner.succeed();
+
+ const addType = abortIfCancel(
+ await select({
+ message: "What do you want to add to your project?",
+ options: [
+ // if there are pages available to install, show them first
+ ...(categories.includes("page") ? [{ label: "Page", value: "page" }] : []),
+
+ // only show schema option if there is at least one data source
+ ...(settings.dataSources.length > 0
+ ? [
+ {
+ label: "Schema",
+ value: "schema",
+ hint: "load data from a new table or layout from an existing data source",
+ },
+ ]
+ : []),
+
+ {
+ label: "Data Source",
+ value: "data",
+ hint: "to connect to a new database or FileMaker file",
+ },
+
+ // show the rest of the categories
+ ...categories
+ .filter((category) => category !== "page")
+ .map((category) => ({
+ label: capitalize(category),
+ value: category,
+ })),
+ ],
+ }),
+ );
+
+ if (addType === "schema") {
+ await runAddSchemaAction();
+ } else if (addType === "data") {
+ await runAddDataSourceCommand();
+ } else if ((categories as string[]).includes(addType)) {
+ // one of the categories
+ const itemsFromCategory = groupedByCategory[addType as keyof typeof groupedByCategory];
+
+ const itemName = abortIfCancel(
+ await select({
+ message: `Select a ${addType} to add to your project`,
+ options: itemsFromCategory.map((item) => ({
+ label: item.title,
+ hint: item.description,
+ value: item.name,
+ })),
+ }),
+ );
+
+ await installFromRegistry(itemName);
+ } else {
+ logger.error(`Could not find any available components in the category "${addType}"`);
+ }
+};
+
+export const runAdd = async (name: string | undefined, options?: { noInstall?: boolean }) => {
+ if (name === "tanstack-query") {
+ return await runAddTanstackQueryCommand();
+ }
+ if (name !== undefined) {
+ // an arbitrary name was provided, so we'll try to install from the registry
+ return await installFromRegistry(name);
+ }
+
+ let settings: Settings;
+ try {
+ settings = getSettings();
+ } catch {
+ await preflightAddCommand();
+ return await runAddFromRegistry(options);
+ }
+
+ if (settings.ui === "shadcn") {
+ return await runAddFromRegistry(options);
+ }
+ ensureProofKitProject({ commandName: "add" });
+
+ const addType = abortIfCancel(
+ await select({
+ message: "What do you want to add to your project?",
+ options: [
+ { label: "Page", value: "page" },
+ // only show schema option if there is at least one data source
+ ...(settings.dataSources.length > 0
+ ? [
+ {
+ label: "Schema",
+ value: "schema",
+ hint: "load data from a new table or layout from an existing data source",
+ },
+ ]
+ : []),
+ { label: "React Email", value: "react-email" },
+ {
+ label: "Data Source",
+ value: "data",
+ hint: "to connect to a new database or FileMaker file",
+ },
+ ...(settings.auth.type === "none" && settings.appType === "browser" ? [{ label: "Auth", value: "auth" }] : []),
+ ],
+ }),
+ );
+
+ if (addType === "auth") {
+ await runAddAuthAction();
+ } else if (addType === "data") {
+ await runAddDataSourceCommand();
+ } else if (addType === "page") {
+ await runAddPageAction();
+ } else if (addType === "schema") {
+ await runAddSchemaAction();
+ } else if (addType === "react-email") {
+ await runAddReactEmailCommand({ noInstall: options?.noInstall });
+ }
+};
+
+export const makeAddCommand = () => {
+ const addCommand = new Command("add")
+ .description("Add a new component to your project")
+ .argument("[name]", "Type of component to add")
+ .addOption(ciOption)
+ .addOption(nonInteractiveOption)
+ .addOption(debugOption)
+ .option("--noInstall", "Do not run your package manager install command", false)
+ .action(async (name, options) => {
+ await runAdd(name, options);
+ });
+
+ addCommand.hook("preAction", (_thisCommand, _actionCommand) => {
+ // console.log("preAction", _actionCommand.opts());
+ initProgramState(_actionCommand.opts());
+ state.baseCommand = "add";
+ });
+ addCommand.hook("preSubcommand", (_thisCommand, _subCommand) => {
+ // console.log("preSubcommand", _subCommand.opts());
+ initProgramState(_subCommand.opts());
+ state.baseCommand = "add";
+ });
+
+ addCommand.addCommand(makeAddAuthCommand());
+ addCommand.addCommand(makeAddPageCommand());
+ addCommand.addCommand(makeAddSchemaCommand());
+ addCommand.addCommand(makeAddDataSourceCommand());
+ return addCommand;
+};
diff --git a/packages/cli-old/src/cli/add/page/index.ts b/packages/cli-old/src/cli/add/page/index.ts
new file mode 100644
index 00000000..8ed95b1d
--- /dev/null
+++ b/packages/cli-old/src/cli/add/page/index.ts
@@ -0,0 +1,230 @@
+import path from "node:path";
+import chalk from "chalk";
+import { Command } from "commander";
+import { capitalize } from "es-toolkit";
+import fs from "fs-extra";
+import { nextjsTemplates, wvTemplates } from "~/cli/add/page/templates.js";
+import * as p from "~/cli/prompts.js";
+import { PKG_ROOT } from "~/consts.js";
+import { getExistingSchemas } from "~/generators/fmdapi.js";
+import { addRouteToNav } from "~/generators/route.js";
+import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js";
+import { initProgramState, isNonInteractiveMode, state } from "~/state.js";
+import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
+import { type DataSource, getSettings, mergeSettings } from "~/utils/parseSettings.js";
+import { abortIfCancel, ensureProofKitProject } from "../../utils.js";
+
+export const runAddPageAction = async (opts?: {
+ routeName?: string;
+ pageName?: string;
+ dataSourceName?: string;
+ schemaName?: string;
+ template?: string;
+}) => {
+ const projectDir = state.projectDir;
+
+ const settings = getSettings();
+ if (settings.ui === "shadcn") {
+ return p.cancel("Adding pages is not yet supported for shadcn-based projects.");
+ }
+
+ const templates = state.appType === "browser" ? Object.entries(nextjsTemplates) : Object.entries(wvTemplates);
+
+ if (templates.length === 0) {
+ return p.cancel("No templates found for your app type. Check back soon!");
+ }
+
+ let routeName = opts?.routeName;
+ let replacedMainPage = settings.replacedMainPage;
+
+ if (state.appType === "webviewer" && !replacedMainPage && !isNonInteractiveMode() && !routeName) {
+ const replaceMainPage = abortIfCancel(
+ await p.select({
+ message: "Do you want to replace the default page?",
+ options: [
+ { label: "Yes", value: "yes" },
+ { label: "No, maybe later", value: "no" },
+ { label: "No, don't ask again", value: "never" },
+ ],
+ }),
+ );
+ if (replaceMainPage === "never" || replaceMainPage === "yes") {
+ replacedMainPage = true;
+ }
+
+ if (replaceMainPage === "yes") {
+ routeName = "/";
+ }
+ }
+
+ if (!routeName) {
+ routeName = abortIfCancel(
+ await p.text({
+ message: "Enter the URL PATH for your new page",
+ placeholder: "/my-page",
+ validate: (value) => {
+ if (value.length === 0) {
+ return "URL path is required";
+ }
+ return;
+ },
+ }),
+ );
+ }
+
+ if (!routeName.startsWith("/")) {
+ routeName = `/${routeName}`;
+ }
+
+ const pageName = capitalize(routeName.replace("/", "").trim());
+
+ const template =
+ opts?.template ??
+ abortIfCancel(
+ await p.select({
+ message: "What template should be used for this page?",
+ options: templates.map(([key, value]) => ({
+ value: key,
+ label: `${value.label}`,
+ hint: value.hint,
+ })),
+ }),
+ );
+
+ const pageTemplate = templates.find(([key]) => key === template)?.[1];
+ if (!pageTemplate) {
+ return p.cancel(`Page template ${template} not found`);
+ }
+
+ let dataSource: DataSource | undefined;
+ let schemaName: string | undefined;
+ if (pageTemplate.requireData) {
+ if (settings.dataSources.length === 0) {
+ return p.cancel(
+ "This template requires a data source, but you don't have any. Add a data source first, or choose another page template",
+ );
+ }
+
+ const dataSourceName =
+ opts?.dataSourceName ??
+ (settings.dataSources.length > 1
+ ? abortIfCancel(
+ await p.select({
+ message: "Which data source should be used for this page?",
+ options: settings.dataSources.map((dataSource) => ({
+ value: dataSource.name,
+ label: dataSource.name,
+ })),
+ }),
+ )
+ : settings.dataSources[0]?.name);
+
+ dataSource = settings.dataSources.find((dataSource) => dataSource.name === dataSourceName);
+ if (!dataSource) {
+ return p.cancel(`Data source ${dataSourceName} not found`);
+ }
+
+ schemaName = await promptForSchemaFromDataSource({
+ projectDir,
+ dataSource,
+ });
+ }
+
+ const spinner = p.spinner();
+ spinner.start("Adding page from template");
+
+ // copy template files
+ const templatePath = path.join(PKG_ROOT, "template/pages", pageTemplate.templatePath);
+
+ const destPath =
+ state.appType === "browser"
+ ? path.join(projectDir, "src/app/(main)", routeName)
+ : path.join(projectDir, "src/routes", routeName);
+
+ await fs.copy(templatePath, destPath);
+
+ if (state.appType === "browser") {
+ if (pageName && pageName !== "") {
+ await addRouteToNav({
+ projectDir: process.cwd(),
+ navType: "primary",
+ label: pageName,
+ href: routeName,
+ });
+ }
+ } else if (state.appType === "webviewer") {
+ // TODO: implement
+ }
+ // call post-install function
+ await pageTemplate.postIntallFn?.({
+ projectDir,
+ pageDir: destPath,
+ dataSource,
+ schemaName,
+ });
+
+ if (replacedMainPage !== settings.replacedMainPage) {
+ // avoid changing this until the end since the user could cancel early
+ mergeSettings({ replacedMainPage });
+ }
+
+ spinner.stop("Added page!");
+ const pkgManager = getUserPkgManager();
+
+ console.log(
+ `\n${chalk.green("Next steps:")}\nTo preview this page, restart your dev server using the ${chalk.cyan(`${pkgManager === "npm" ? "npm run" : pkgManager} dev`)} command\n`,
+ );
+};
+
+export const makeAddPageCommand = () => {
+ const addPageCommand = new Command("page").description("Add a new page to your project").action(async () => {
+ await runAddPageAction();
+ });
+
+ addPageCommand.addOption(ciOption);
+ addPageCommand.addOption(nonInteractiveOption);
+ addPageCommand.addOption(debugOption);
+
+ addPageCommand.hook("preAction", () => {
+ initProgramState(addPageCommand.opts());
+ state.baseCommand = "add";
+ ensureProofKitProject({ commandName: "add" });
+ });
+
+ return addPageCommand;
+};
+
+async function promptForSchemaFromDataSource({
+ projectDir = process.cwd(),
+ dataSource,
+}: {
+ projectDir?: string;
+ dataSource: DataSource;
+}) {
+ if (dataSource.type === "supabase") {
+ throw new Error("Not implemented");
+ }
+ const schemas = getExistingSchemas({
+ projectDir,
+ dataSourceName: dataSource.name,
+ })
+ .map((s) => s.schemaName)
+ .filter(Boolean);
+
+ if (schemas.length === 0) {
+ p.cancel("This data source doesn't have any schemas to load data from");
+ return undefined;
+ }
+
+ if (schemas.length === 1) {
+ return schemas[0];
+ }
+
+ const schemaName = abortIfCancel(
+ await p.select({
+ message: "Which schema should this page load data from?",
+ options: schemas.map((schema) => ({ label: schema ?? "", value: schema ?? "" })),
+ }),
+ );
+ return schemaName;
+}
diff --git a/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts b/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts
new file mode 100644
index 00000000..fcccbb27
--- /dev/null
+++ b/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts
@@ -0,0 +1,12 @@
+import { injectTanstackQuery } from "~/generators/tanstack-query.js";
+import { installDependencies } from "~/helpers/installDependencies.js";
+import type { TPostInstallFn } from "../types.js";
+import { postInstallTable } from "./table.js";
+
+export const postInstallTableInfinite: TPostInstallFn = async (args) => {
+ await postInstallTable(args);
+ const didInject = await injectTanstackQuery();
+ if (didInject) {
+ await installDependencies();
+ }
+};
diff --git a/packages/cli-old/src/cli/add/page/post-install/table.ts b/packages/cli-old/src/cli/add/page/post-install/table.ts
new file mode 100644
index 00000000..a6e930ed
--- /dev/null
+++ b/packages/cli-old/src/cli/add/page/post-install/table.ts
@@ -0,0 +1,123 @@
+import path from "node:path";
+import fs from "fs-extra";
+import { SyntaxKind } from "ts-morph";
+
+import { getClientSuffix, getFieldNamesForSchema } from "~/generators/fmdapi.js";
+import { injectTanstackQuery } from "~/generators/tanstack-query.js";
+import { installDependencies } from "~/helpers/installDependencies.js";
+import { state } from "~/state.js";
+import { getSettings } from "~/utils/parseSettings.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+import type { TPostInstallFn } from "../types.js";
+
+// Regex to validate JavaScript identifiers
+const VALID_JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
+
+export const postInstallTable: TPostInstallFn = async ({ projectDir, pageDir, dataSource, schemaName }) => {
+ if (!dataSource) {
+ throw new Error("DataSource is required for table page");
+ }
+ if (!schemaName) {
+ throw new Error("SchemaName is required for table page");
+ }
+ if (dataSource.type !== "fm") {
+ throw new Error("FileMaker DataSource is required for table page");
+ }
+
+ const clientSuffix = getClientSuffix({
+ projectDir,
+ dataSourceName: dataSource.name,
+ });
+
+ const allFieldNames = getFieldNamesForSchema({
+ schemaName,
+ dataSourceName: dataSource.name,
+ });
+
+ const settings = getSettings();
+ if (settings.ui === "shadcn") {
+ return;
+ }
+ const auth = settings.auth;
+
+ const substitutions = {
+ __SOURCE_NAME__: dataSource.name,
+ __TYPE_NAME__: `T${schemaName}`,
+ __ZOD_TYPE_NAME__: `Z${schemaName}`,
+ __CLIENT_NAME__: `${schemaName}${clientSuffix}`,
+ __SCHEMA_NAME__: schemaName,
+ __ACTION_CLIENT__: auth.type === "none" ? "actionClient" : "authedActionClient",
+ __FIRST_FIELD_NAME__: allFieldNames[0] ?? "NO_FIELDS_ON_YOUR_LAYOUT",
+ };
+
+ // read all files in pageDir and loop over them
+ const files = await fs.readdir(pageDir);
+ for await (const file of files) {
+ const filePath = path.join(pageDir, file);
+ let fileContent = await fs.readFile(filePath, "utf8");
+
+ for (const [key, value] of Object.entries(substitutions)) {
+ fileContent = fileContent.replace(new RegExp(key, "g"), value);
+ }
+
+ await fs.writeFile(filePath, fileContent, "utf8");
+ }
+
+ // add the schemas to the columns array
+ const project = getNewProject(projectDir);
+ const sourceFile = project.addSourceFileAtPath(
+ path.join(pageDir, state.appType === "browser" ? "table.tsx" : "index.tsx"),
+ );
+ const columns = sourceFile.getVariableDeclaration("columns")?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
+
+ const fieldNames = filterOutCommonFieldNames(allFieldNames.filter(Boolean) as string[]);
+
+ for await (const fieldName of fieldNames) {
+ columns?.addElement((writer) =>
+ writer
+ .inlineBlock(() => {
+ if (needsBracketNotation(fieldName)) {
+ writer.write(`accessorFn: (row) => row["${fieldName}"],`);
+ } else {
+ writer.write(`accessorFn: (row) => row.${fieldName},`);
+ }
+ writer.write(`header: "${fieldName}",`);
+ })
+ .write(",")
+ .newLine(),
+ );
+ }
+
+ if (state.appType === "webviewer") {
+ const didInject = await injectTanstackQuery({ project });
+ if (didInject) {
+ await installDependencies();
+ }
+ }
+
+ await formatAndSaveSourceFiles(project);
+};
+
+// Function to check if a field name needs bracket notation
+function needsBracketNotation(fieldName: string): boolean {
+ // Check if it's a valid JavaScript identifier
+ return !VALID_JS_IDENTIFIER.test(fieldName);
+}
+
+const commonFieldNamesToExclude = [
+ "id",
+ "pk",
+ "createdat",
+ "updatedat",
+ "primarykey",
+ "createdby",
+ "modifiedby",
+ "creationtimestamp",
+ "modificationtimestamp",
+];
+
+function filterOutCommonFieldNames(fieldNames: string[]): string[] {
+ return fieldNames.filter(
+ (fieldName) => !commonFieldNamesToExclude.includes(fieldName.toLowerCase()) || fieldName.startsWith("_"),
+ );
+}
diff --git a/packages/cli-old/src/cli/add/page/templates.ts b/packages/cli-old/src/cli/add/page/templates.ts
new file mode 100644
index 00000000..a49d5740
--- /dev/null
+++ b/packages/cli-old/src/cli/add/page/templates.ts
@@ -0,0 +1,85 @@
+import { postInstallTable } from "./post-install/table.js";
+import { postInstallTableInfinite } from "./post-install/table-infinite.js";
+import type { TPostInstallFn } from "./types.js";
+
+export interface Template {
+ requireData: boolean;
+ label: string;
+ hint?: string;
+ templatePath: string;
+ screenshot?: string;
+ tags?: string[];
+ postIntallFn?: TPostInstallFn;
+}
+
+export const nextjsTemplates: Record = {
+ blank: {
+ requireData: false,
+ label: "Blank",
+ templatePath: "nextjs/blank",
+ },
+ table: {
+ requireData: true,
+ label: "Basic Table",
+ hint: "Use to load and show multiple records",
+ templatePath: "nextjs/table",
+ postIntallFn: postInstallTable,
+ },
+ tableEdit: {
+ requireData: true,
+ label: "Basic Table (editable)",
+ hint: "Use to load and show multiple records with inline edit functionality",
+ templatePath: "nextjs/table-edit",
+ postIntallFn: postInstallTable,
+ },
+ tableInfinite: {
+ requireData: true,
+ label: "Infinite Table",
+ hint: "Automatically load more records when the user scrolls to the bottom",
+ templatePath: "nextjs/table-infinite",
+ postIntallFn: postInstallTableInfinite,
+ },
+ tableInfiniteEdit: {
+ requireData: true,
+ label: "Infinite Table (editable)",
+ hint: "Automatically load more records when the user scrolls to the bottom with inline edit functionality",
+ templatePath: "nextjs/table-infinite-edit",
+ postIntallFn: postInstallTableInfinite,
+ },
+};
+
+export const wvTemplates: Record = {
+ blank: {
+ requireData: false,
+ label: "Blank",
+ templatePath: "vite-wv/blank",
+ },
+ table: {
+ requireData: true,
+ label: "Basic Table",
+ hint: "Use to load and show multiple records",
+ templatePath: "vite-wv/table",
+ postIntallFn: postInstallTable,
+ },
+ tableEdit: {
+ requireData: true,
+ label: "Basic Table (editable)",
+ hint: "Use to load and show multiple records with inline edit functionality",
+ templatePath: "vite-wv/table-edit",
+ postIntallFn: postInstallTable,
+ },
+ // tableInfinite: {
+ // requireData: true,
+ // label: "Infinite Table",
+ // hint: "Automatically load more records when the user scrolls to the bottom",
+ // templatePath: "vite-wv/table-infinite",
+ // postIntallFn: postInstallTableInfinite,
+ // },
+ // tableInfiniteEdit: {
+ // requireData: true,
+ // label: "Infinite Table (editable)",
+ // hint: "Automatically load more records when the user scrolls to the bottom with inline edit functionality",
+ // templatePath: "vite-wv/table-infinite-edit",
+ // postIntallFn: postInstallTableInfinite,
+ // },
+};
diff --git a/packages/cli-old/src/cli/add/page/types.ts b/packages/cli-old/src/cli/add/page/types.ts
new file mode 100644
index 00000000..7b7da162
--- /dev/null
+++ b/packages/cli-old/src/cli/add/page/types.ts
@@ -0,0 +1,19 @@
+import type { DataSource } from "~/utils/parseSettings.js";
+
+export type TPostInstallFn = (args: {
+ projectDir: string;
+ /** Path in the project where the pages were copyied to. */
+ pageDir: string;
+ dataSource?: DataSource;
+ schemaName?: string;
+}) => void | Promise;
+
+export interface Template {
+ requireData: boolean;
+ label: string;
+ hint?: string;
+ /** Path from the template/pages directory to the template files to copy. */
+ templatePath: string;
+ /** Will be run after the page contents is created and copied into the project. */
+ postIntallFn?: TPostInstallFn;
+}
diff --git a/packages/cli-old/src/cli/add/registry/getOptions.ts b/packages/cli-old/src/cli/add/registry/getOptions.ts
new file mode 100644
index 00000000..9e778b10
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/getOptions.ts
@@ -0,0 +1,44 @@
+import path from "node:path";
+import fg from "fast-glob";
+import fs from "fs-extra";
+
+import { state } from "~/state.js";
+import { registryFetch } from "./http.js";
+
+export async function getMetaFromRegistry(name: string) {
+ const result = await registryFetch("@get/meta/:name", {
+ params: { name },
+ });
+
+ if (result.error) {
+ if (result.error.status === 404) {
+ return null;
+ }
+ throw new Error(result.error.message);
+ }
+
+ return result.data;
+}
+
+const PROJECT_SHARED_IGNORE = ["**/node_modules/**", ".next", "public", "dist", "build"];
+
+export async function getProjectInfo() {
+ const cwd = state.projectDir || process.cwd();
+ const [configFiles, isSrcDir] = await Promise.all([
+ fg.glob("**/{next,vite,astro,app}.config.*|gatsby-config.*|composer.json|react-router.config.*", {
+ cwd,
+ deep: 3,
+ ignore: PROJECT_SHARED_IGNORE,
+ }),
+ fs.pathExists(path.resolve(cwd, "src")),
+ ]);
+
+ const isUsingAppDir = await fs.pathExists(path.resolve(cwd, `${isSrcDir ? "src/" : ""}app`));
+
+ // Next.js.
+ if (configFiles.find((file) => file.startsWith("next.config."))?.length) {
+ return isUsingAppDir ? "next-app" : "next-pages";
+ }
+
+ return "manual";
+}
diff --git a/packages/cli-old/src/cli/add/registry/http.ts b/packages/cli-old/src/cli/add/registry/http.ts
new file mode 100644
index 00000000..5625d73b
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/http.ts
@@ -0,0 +1,18 @@
+import { createFetch, createSchema } from "@better-fetch/fetch";
+import { registryIndexSchema, templateMetadataSchema } from "@proofkit/registry";
+
+import { getRegistryUrl } from "~/helpers/shadcn-cli.js";
+
+const schema = createSchema({
+ "@get/meta/:name": {
+ output: templateMetadataSchema,
+ },
+ "@get/": {
+ output: registryIndexSchema,
+ },
+});
+
+export const registryFetch = createFetch({
+ baseURL: `${getRegistryUrl()}/r`,
+ schema,
+});
diff --git a/packages/cli-old/src/cli/add/registry/install.ts b/packages/cli-old/src/cli/add/registry/install.ts
new file mode 100644
index 00000000..368a84a0
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/install.ts
@@ -0,0 +1,224 @@
+import { getOtherProofKitDependencies } from "@proofkit/registry";
+import { capitalize, uniq } from "es-toolkit";
+import ora from "ora";
+import semver from "semver";
+import * as p from "~/cli/prompts.js";
+
+import { abortIfCancel } from "~/cli/utils.js";
+import { getExistingSchemas } from "~/generators/fmdapi.js";
+import { addRouteToNav } from "~/generators/route.js";
+import { getRegistryUrl, shadcnInstall } from "~/helpers/shadcn-cli.js";
+import { state } from "~/state.js";
+import { getVersion } from "~/utils/getProofKitVersion.js";
+import { logger } from "~/utils/logger.js";
+import { type DataSource, getSettings, mergeSettings } from "~/utils/parseSettings.js";
+import { getMetaFromRegistry } from "./getOptions.js";
+import { buildHandlebarsData, randerHandlebarsToFile } from "./postInstall/handlebars.js";
+import { processPostInstallStep } from "./postInstall/index.js";
+import { preflightAddCommand } from "./preflight.js";
+
+async function promptForSchemaFromDataSource({
+ projectDir = process.cwd(),
+ dataSource,
+}: {
+ projectDir?: string;
+ dataSource: DataSource;
+}) {
+ if (dataSource.type === "supabase") {
+ throw new Error("Not implemented");
+ }
+ const schemas = getExistingSchemas({
+ projectDir,
+ dataSourceName: dataSource.name,
+ })
+ .map((s) => s.schemaName)
+ .filter(Boolean);
+
+ if (schemas.length === 0) {
+ p.cancel("This data source doesn't have any schemas to load data from");
+ return undefined;
+ }
+
+ if (schemas.length === 1) {
+ return schemas[0];
+ }
+
+ const schemaName = abortIfCancel(
+ await p.select({
+ message: "Which schema should this template use?",
+ options: schemas.map((schema) => ({ label: schema ?? "", value: schema ?? "" })),
+ }),
+ );
+ return schemaName;
+}
+
+export async function installFromRegistry(name: string) {
+ const spinner = ora("Validating template").start();
+
+ try {
+ await preflightAddCommand();
+ const meta = await getMetaFromRegistry(name);
+ if (!meta) {
+ spinner.fail(`Template ${name} not found in the ProofKit registry`);
+ return;
+ }
+
+ if (meta.minimumProofKitVersion && semver.gt(meta.minimumProofKitVersion, getVersion())) {
+ logger.error(
+ `Template ${name} requires ProofKit version ${meta.minimumProofKitVersion}, but you are using version ${getVersion()}`,
+ );
+ spinner.fail("Template is not compatible with your ProofKit version");
+ return;
+ }
+ spinner.succeed();
+
+ const otherProofKitDependencies = getOtherProofKitDependencies(meta);
+ let previouslyInstalledTemplates = getSettings().registryTemplates;
+
+ // Handle schema requirement if template needs it
+ let dataSource: DataSource | undefined;
+ let schemaName: string | undefined;
+ let routeName: string | undefined;
+ let pageName: string | undefined;
+
+ if (meta.schemaRequired) {
+ const settings = getSettings();
+
+ if (settings.dataSources.length === 0) {
+ spinner.fail("This template requires a data source, but you don't have any. Add a data source first.");
+ return;
+ }
+
+ const dataSourceName =
+ settings.dataSources.length > 1
+ ? abortIfCancel(
+ await p.select({
+ message: "Which data source should be used for this template?",
+ options: settings.dataSources.map((ds) => ({
+ value: ds.name,
+ label: ds.name,
+ })),
+ }),
+ )
+ : settings.dataSources[0]?.name;
+
+ dataSource = settings.dataSources.find((ds) => ds.name === dataSourceName);
+
+ if (!dataSource) {
+ spinner.fail(`Data source ${dataSourceName} not found`);
+ return;
+ }
+
+ schemaName = await promptForSchemaFromDataSource({
+ projectDir: state.projectDir,
+ dataSource,
+ });
+
+ if (!schemaName) {
+ spinner.fail("Schema selection was cancelled");
+ return;
+ }
+ }
+
+ if (meta.category === "page") {
+ // Prompt user for the URL path of the page
+ routeName = abortIfCancel(
+ await p.text({
+ message: "Enter the URL PATH for your new page",
+ placeholder: "/my-page",
+ validate: (value) => {
+ if (value.length === 0) {
+ return "URL path is required";
+ }
+ return;
+ },
+ }),
+ );
+
+ if (routeName.startsWith("/")) {
+ routeName = routeName.slice(1);
+ }
+
+ pageName = capitalize(routeName.replace("/", "").trim());
+ }
+
+ const url = new URL(`${getRegistryUrl()}/r/${name}`);
+ if (meta.category === "page") {
+ url.searchParams.set("routeName", `/(main)/${routeName ?? name}`);
+ }
+
+ // a (hopefully) temporary workaround because the shadcn command installs the env file in the wrong place if it's a dependency
+ if (
+ name === "fmdapi" &&
+ !previouslyInstalledTemplates.includes("utils/t3-env") &&
+ // this last guard will allow this workaroudn to be bypassed if the registry server updates to start serving the dependency again
+ meta.registryDependencies?.find((d) => d.includes("utils/t3-env")) === undefined
+ ) {
+ // install the t3-env template manually first
+ await installFromRegistry("utils/t3-env");
+ previouslyInstalledTemplates = getSettings().registryTemplates;
+ }
+
+ // now install the template using shadcn-install
+ await shadcnInstall([url.toString()], meta.title);
+
+ const handlebarsFiles = meta.files.filter((file) => file.handlebars);
+
+ if (handlebarsFiles.length > 0) {
+ // Build template data with schema information if available
+ const baseTemplateData =
+ dataSource && schemaName
+ ? buildHandlebarsData({
+ dataSource,
+ schemaName,
+ })
+ : buildHandlebarsData();
+
+ // Add page information to template data if available
+ const templateData = {
+ ...baseTemplateData,
+ ...(routeName && { routeName }),
+ ...(pageName && { pageName }),
+ };
+
+ // Resolve __PATH__ placeholders in file paths before handlebars processing
+ const resolvedFiles = handlebarsFiles.map((file) => ({
+ ...file,
+ destinationPath: file.destinationPath?.replace("__PATH__", `/(main)/${routeName ?? name}`),
+ }));
+
+ for (const file of resolvedFiles) {
+ await randerHandlebarsToFile(file, templateData);
+ }
+ }
+
+ // Add route to navigation if this is a page template
+ if (meta.category === "page" && routeName && pageName) {
+ await addRouteToNav({
+ projectDir: state.projectDir,
+ navType: "primary",
+ label: pageName,
+ href: `/${routeName}`,
+ });
+ }
+
+ // if post-install steps, process those
+ if (meta.postInstall) {
+ for (const step of meta.postInstall) {
+ if (step._from && previouslyInstalledTemplates.includes(step._from)) {
+ // don't re-run post-install steps for templates that have already been installed
+ continue;
+ }
+ await processPostInstallStep(step);
+ }
+ }
+
+ // update the settings
+ mergeSettings({
+ registryTemplates: uniq([...previouslyInstalledTemplates, name, ...otherProofKitDependencies]),
+ });
+ } catch (error) {
+ spinner.fail("Failed to fetch template metadata.");
+ logger.error(error);
+ }
+}
diff --git a/packages/cli-old/src/cli/add/registry/listItems.ts b/packages/cli-old/src/cli/add/registry/listItems.ts
new file mode 100644
index 00000000..046f5c73
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/listItems.ts
@@ -0,0 +1,9 @@
+import { registryFetch } from "./http.js";
+
+export async function listItems() {
+ const { data: items, error } = await registryFetch("@get/");
+ if (error) {
+ throw new Error(`Failed to fetch items from registry: ${error.message}`);
+ }
+ return items;
+}
diff --git a/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts b/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts
new file mode 100644
index 00000000..2ef0b79c
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts
@@ -0,0 +1,189 @@
+import path from "node:path";
+import { decodeHandlebarsFromShadcn, type TemplateFile } from "@proofkit/registry";
+import fs from "fs-extra";
+import handlebars from "handlebars";
+import { getClientSuffix, getFieldNamesForSchema } from "~/generators/fmdapi.js";
+import { getShadcnConfig } from "~/helpers/shadcn-cli.js";
+import { state } from "~/state.js";
+import { type DataSource, getSettings } from "~/utils/parseSettings.js";
+
+// Register handlebars helpers
+handlebars.registerHelper("eq", (a, b) => a === b);
+
+interface HandlebarsContext {
+ [key: string]: unknown;
+}
+
+handlebars.registerHelper("findFirst", function (this: HandlebarsContext, array, predicate, options) {
+ if (!(array && Array.isArray(array))) {
+ return options.inverse(this);
+ }
+
+ for (const item of array) {
+ if (predicate === "fm" && item.type === "fm") {
+ return options.fn(item);
+ }
+ }
+ return options.inverse(this);
+});
+
+interface DataSourceForTemplate {
+ dataSource: DataSource;
+ schemaName: string;
+}
+
+const commonFieldNamesToExclude = [
+ "id",
+ "pk",
+ "createdat",
+ "updatedat",
+ "primarykey",
+ "createdby",
+ "modifiedby",
+ "creationtimestamp",
+ "modificationtimestamp",
+];
+
+function filterOutCommonFieldNames(fieldNames: string[]): string[] {
+ return fieldNames.filter(
+ (fieldName) => !commonFieldNamesToExclude.includes(fieldName.toLowerCase()) || fieldName.startsWith("_"),
+ );
+}
+
+function buildDataSourceData(args: DataSourceForTemplate) {
+ const { dataSource, schemaName } = args;
+
+ const clientSuffix = getClientSuffix({
+ projectDir: state.projectDir ?? process.cwd(),
+ dataSourceName: dataSource.name,
+ });
+
+ const allFieldNames = getFieldNamesForSchema({
+ schemaName,
+ dataSourceName: dataSource.name,
+ }).filter(Boolean) as string[];
+
+ return {
+ sourceName: dataSource.name,
+ schemaName,
+ clientSuffix,
+ allFieldNames,
+ fieldNames: filterOutCommonFieldNames(allFieldNames),
+ };
+}
+
+export function buildHandlebarsData(args?: DataSourceForTemplate) {
+ const proofkit = getSettings();
+ const shadcn = getShadcnConfig();
+
+ return {
+ proofkit,
+ shadcn,
+ schema: args
+ ? buildDataSourceData(args)
+ : {
+ sourceName: "UnknownDataSource",
+ schemaName: "UnknownSchema",
+ clientSuffix: "UnknownClientSuffix",
+ allFieldNames: ["UnknownFieldName"],
+ fieldNames: ["UnknownFieldName"],
+ },
+ };
+}
+
+export async function randerHandlebarsToFile(file: TemplateFile, data: ReturnType) {
+ const inputPath = getFilePath(file, data);
+ let rawTemplate = await fs.readFile(inputPath, "utf8");
+
+ // Decode placeholder tokens back to handlebars syntax
+ // This uses the centralized decoding function from the registry package
+ rawTemplate = decodeHandlebarsFromShadcn(rawTemplate);
+
+ const template = handlebars.compile(rawTemplate);
+ const rendered = template(data);
+ await fs.writeFile(inputPath, rendered);
+}
+
+export function getFilePath(file: TemplateFile, data: ReturnType): string {
+ const thePath = file.sourceFileName;
+
+ if (file.destinationPath) {
+ return file.destinationPath;
+ }
+
+ const cwd = state.projectDir ?? process.cwd();
+ const { shadcn } = data;
+
+ // Create a mapping between registry types and their corresponding shadcn config aliases
+ let blockAlias = "src/components/blocks";
+ if (shadcn?.aliases?.components) {
+ if (shadcn.aliases.components.startsWith("@/")) {
+ blockAlias = `${shadcn.aliases.components.replace("@/", "src/")}/blocks`;
+ } else {
+ blockAlias = `src/${shadcn.aliases.components}/blocks`;
+ }
+ }
+
+ const typeToAliasMap: Record = {
+ "registry:lib": shadcn?.aliases?.lib || shadcn?.aliases?.utils,
+ "registry:component": shadcn?.aliases?.components,
+ "registry:ui": shadcn?.aliases?.ui || shadcn?.aliases?.components,
+ "registry:hook": shadcn?.aliases?.hooks,
+ // These types don't have direct aliases, so we use fallback paths
+ "registry:file": "src",
+ "registry:page": "src/app",
+ "registry:block": blockAlias,
+ "registry:theme": "src/theme",
+ "registry:style": "src/styles",
+ };
+
+ const aliasPath = typeToAliasMap[file.type];
+
+ if (aliasPath) {
+ // Handle @/ prefix which represents the src directory
+ if (aliasPath.startsWith("@/")) {
+ const resolvedPath = aliasPath.replace("@/", "src/");
+ return path.join(cwd, resolvedPath, thePath);
+ }
+ // If the alias starts with a path separator or contains src/, treat it as a relative path from cwd
+ if (aliasPath.startsWith("/") || aliasPath.includes("src/")) {
+ return path.join(cwd, aliasPath, thePath);
+ }
+ // Otherwise, treat it as an alias that should be resolved relative to src/
+
+ return path.join(cwd, "src", aliasPath, thePath);
+ }
+
+ // Fallback to hardcoded paths for unsupported types
+ switch (file.type) {
+ case "registry:lib":
+ return path.join(cwd, "src", "lib", thePath);
+ case "registry:file":
+ return path.join(cwd, "src", thePath);
+ case "registry:page": {
+ // For page templates, use the route name if available in template data
+ const routeName = "routeName" in data ? (data.routeName as string) : undefined;
+ if (routeName) {
+ // Add /(main) prefix for Next.js app router structure
+ const pageRoute = routeName === "/" ? "" : routeName;
+ return path.join(cwd, "src", "app", "(main)", pageRoute, thePath);
+ }
+ return path.join(cwd, "src", "app", thePath);
+ }
+ case "registry:block":
+ return path.join(cwd, "src", "components", "blocks", thePath);
+ case "registry:component":
+ return path.join(cwd, "src", "components", thePath);
+ case "registry:ui":
+ return path.join(cwd, "src", "components", thePath);
+ case "registry:hook":
+ return path.join(cwd, "src", "hooks", thePath);
+ case "registry:theme":
+ return path.join(cwd, "src", "theme", thePath);
+ case "registry:style":
+ return path.join(cwd, "src", "styles", thePath);
+ default:
+ // default to source file name
+ return thePath;
+ }
+}
diff --git a/packages/cli-old/src/cli/add/registry/postInstall/index.ts b/packages/cli-old/src/cli/add/registry/postInstall/index.ts
new file mode 100644
index 00000000..6afb4233
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/postInstall/index.ts
@@ -0,0 +1,22 @@
+import type { PostInstallStep } from "@proofkit/registry";
+
+import { addToEnv } from "~/utils/addToEnvs.js";
+import { logger } from "~/utils/logger.js";
+import { addScriptToPackageJson } from "./package-script.js";
+import { wrapProvider } from "./wrap-provider.js";
+
+export async function processPostInstallStep(step: PostInstallStep) {
+ if (step.action === "package.json script") {
+ addScriptToPackageJson(step);
+ } else if (step.action === "wrap provider") {
+ await wrapProvider(step);
+ } else if (step.action === "next-steps") {
+ logger.info(step.data.message);
+ } else if (step.action === "env") {
+ await addToEnv({
+ envs: step.data.envs,
+ });
+ } else {
+ logger.error(`Unknown post-install step: ${step}`);
+ }
+}
diff --git a/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts b/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts
new file mode 100644
index 00000000..50df220c
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts
@@ -0,0 +1,12 @@
+import fs from "node:fs";
+import path from "node:path";
+import type { PostInstallStep } from "@proofkit/registry";
+
+import { state } from "~/state.js";
+
+export function addScriptToPackageJson(step: Extract) {
+ const packageJsonPath = path.join(state.projectDir, "package.json");
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
+ packageJson.scripts[step.data.scriptName] = step.data.scriptCommand;
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
+}
diff --git a/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts b/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts
new file mode 100644
index 00000000..dd1ec34e
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts
@@ -0,0 +1,132 @@
+import path from "node:path";
+import type { PostInstallStep } from "@proofkit/registry";
+import { type ImportDeclarationStructure, type JsxChild, type JsxElement, StructureKind, SyntaxKind } from "ts-morph";
+
+import { getShadcnConfig } from "~/helpers/shadcn-cli.js";
+import { state } from "~/state.js";
+import { logger } from "~/utils/logger.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+
+export async function wrapProvider(step: Extract) {
+ const { parentTag, imports: importConfigs, providerCloseTag, providerOpenTag } = step.data;
+
+ try {
+ const projectDir = state.projectDir;
+ const project = getNewProject(projectDir);
+ const shadcnConfig = getShadcnConfig();
+
+ // Resolve the components alias to a filesystem path
+ // @/components -> src/components, ./components -> components, etc.
+ const resolveAlias = (alias: string): string => {
+ if (alias.startsWith("@/")) {
+ return alias.replace("@/", "src/");
+ }
+ if (alias.startsWith("./")) {
+ return alias.substring(2);
+ }
+ return alias;
+ };
+
+ // Look for providers.tsx in the components directory
+ const componentsDir = resolveAlias(shadcnConfig.aliases.components);
+ const providersPath = path.join(projectDir, componentsDir, "providers.tsx");
+
+ const providersFile = project.addSourceFileAtPath(providersPath);
+
+ // Add all import statements
+ for (const importConfig of importConfigs) {
+ const importDeclaration: ImportDeclarationStructure = {
+ moduleSpecifier: importConfig.moduleSpecifier,
+ kind: StructureKind.ImportDeclaration,
+ };
+
+ if (importConfig.defaultImport) {
+ importDeclaration.defaultImport = importConfig.defaultImport;
+ }
+
+ if (importConfig.namedImports && importConfig.namedImports.length > 0) {
+ importDeclaration.namedImports = importConfig.namedImports;
+ }
+
+ providersFile.addImportDeclaration(importDeclaration);
+ }
+
+ // Handle providers.tsx file - look for the default export function
+ const exportDefault = providersFile.getFunction((dec) => dec.isDefaultExport());
+
+ if (!exportDefault) {
+ logger.warn(`No default export function found in ${providersPath}`);
+ return;
+ }
+
+ const returnStatement = exportDefault?.getBody()?.getFirstDescendantByKind(SyntaxKind.ReturnStatement);
+
+ if (!returnStatement) {
+ logger.warn("No return statement found in default export function");
+ return;
+ }
+
+ let targetElement: JsxElement | undefined;
+
+ // Try to find the parent tag if specified
+ if (parentTag && parentTag.length > 0) {
+ for (const tag of parentTag) {
+ targetElement = returnStatement
+ ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
+ .find((openingElement) => openingElement.getTagNameNode().getText() === tag)
+ ?.getParentIfKind(SyntaxKind.JsxElement);
+
+ if (targetElement) {
+ break;
+ }
+ }
+ }
+
+ if (targetElement) {
+ // If we found a parent tag, wrap its children
+ const childrenText = targetElement
+ ?.getJsxChildren()
+ .map((child: JsxChild) => child.getText())
+ .filter(Boolean)
+ .join("\n");
+
+ const newContent = `${providerOpenTag}
+ ${childrenText}
+ ${providerCloseTag}`;
+
+ targetElement.getChildSyntaxList()?.replaceWithText(newContent);
+ } else {
+ // If no parent tag found or specified, wrap the entire return statement
+ const returnExpression = returnStatement?.getExpression();
+ if (returnExpression) {
+ // Check if the expression is a ParenthesizedExpression
+ const isParenthesized = returnExpression.getKind() === SyntaxKind.ParenthesizedExpression;
+
+ let innerExpressionText: string;
+ if (isParenthesized) {
+ // Get the inner expression from the parenthesized expression
+ const parenthesizedExpr = returnExpression.asKindOrThrow(SyntaxKind.ParenthesizedExpression);
+ innerExpressionText = parenthesizedExpr.getExpression().getText();
+ } else {
+ innerExpressionText = returnExpression.getText();
+ }
+
+ const newReturnContent = `return (
+ ${providerOpenTag}
+ ${innerExpressionText}
+ ${providerCloseTag}
+ );`;
+
+ returnStatement?.replaceWithText(newReturnContent);
+ } else {
+ logger.warn("No return expression found to wrap");
+ }
+ }
+
+ await formatAndSaveSourceFiles(project);
+ logger.success(`Successfully wrapped provider in ${providersPath}`);
+ } catch (error) {
+ logger.error(`Failed to wrap provider: ${error}`);
+ throw error;
+ }
+}
diff --git a/packages/cli-old/src/cli/add/registry/preflight.ts b/packages/cli-old/src/cli/add/registry/preflight.ts
new file mode 100644
index 00000000..a7e973f0
--- /dev/null
+++ b/packages/cli-old/src/cli/add/registry/preflight.ts
@@ -0,0 +1,17 @@
+import path from "node:path";
+import fs from "fs-extra";
+
+import { stealthInit } from "~/helpers/stealth-init.js";
+import { state } from "~/state.js";
+
+export async function preflightAddCommand() {
+ const cwd = state.projectDir ?? process.cwd();
+ // make sure shadcn is installed, throw if not
+ const shadcnInstalled = await fs.pathExists(path.join(cwd, "components.json"));
+ if (!shadcnInstalled) {
+ throw new Error("Shadcn is not installed. Please run `pnpm dlx shadcn@latest init` to install it.");
+ }
+
+ // if proofkit is not inited, try to stealth init
+ await stealthInit();
+}
diff --git a/packages/cli-old/src/cli/deploy/index.ts b/packages/cli-old/src/cli/deploy/index.ts
new file mode 100644
index 00000000..ecc1deac
--- /dev/null
+++ b/packages/cli-old/src/cli/deploy/index.ts
@@ -0,0 +1,489 @@
+import path from "node:path";
+import chalk from "chalk";
+import { Command, Option } from "commander";
+import { execa } from "execa";
+import fs from "fs-extra";
+import type { PackageJson } from "type-fest";
+import * as p from "~/cli/prompts.js";
+
+import { ciOption, debugOption } from "~/globalOptions.js";
+
+// Regex patterns defined at top level for performance
+const LEADING_SYMBOLS_REGEX = /^[✔\s]+/;
+const MULTI_SPACE_REGEX = /\s{2,}/;
+const VERSION_PREFIX_REGEX = /^v/;
+
+import { initProgramState, state } from "~/state.js";
+import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
+import { getSettings } from "~/utils/parseSettings.js";
+import { ensureProofKitProject } from "../utils.js";
+
+async function checkVercelCLI(): Promise {
+ try {
+ await execa("vercel", ["--version"]);
+ return true;
+ } catch (_error) {
+ return false;
+ }
+}
+
+async function installVercelCLI() {
+ const pkgManager = getUserPkgManager();
+ const spinner = p.spinner();
+ spinner.start("Installing Vercel CLI...");
+
+ try {
+ const installCmd = pkgManager === "npm" ? "install" : "add";
+ await execa(pkgManager, [installCmd, "-g", "vercel"]);
+ spinner.stop("Vercel CLI installed successfully");
+ return true;
+ } catch (error) {
+ spinner.stop("Failed to install Vercel CLI");
+ console.error(chalk.red("Error installing Vercel CLI:"), error);
+ return false;
+ }
+}
+
+async function checkVercelProject(): Promise {
+ try {
+ // Try to read the .vercel/project.json file which exists when a project is linked
+ const projectConfig = (await fs.readJSON(".vercel/project.json")) as VercelProjectConfig;
+ return Boolean(projectConfig.projectId);
+ } catch (_error) {
+ if (state.debug) {
+ console.log("\nDebug: No Vercel project configuration found");
+ }
+ return false;
+ }
+}
+
+async function getVercelTeams(): Promise<{ slug: string; name: string }[]> {
+ try {
+ if (state.debug) {
+ console.log("\nDebug: Running vercel teams list command...");
+ }
+
+ const result = await execa("vercel", ["teams", "list"], {
+ all: true,
+ });
+
+ if (state.debug) {
+ console.log("\nDebug: Command output:", result.all);
+ }
+
+ const lines = (result.all ?? "").split("\n").filter(Boolean);
+
+ // Find the index of the header line
+ const headerIndex = lines.findIndex((line) => line.includes("id"));
+ if (headerIndex === -1) {
+ return [];
+ }
+
+ // Get only the lines after the header
+ const teamLines = lines.slice(headerIndex + 1);
+
+ if (state.debug) {
+ console.log("\nDebug: Team lines:");
+ for (const line of teamLines) {
+ console.log(`"${line}"`);
+ }
+ }
+
+ const teams = teamLines
+ .map((line) => {
+ // Remove any leading symbols (✔ or spaces) and trim
+ const cleanLine = line.replace(LEADING_SYMBOLS_REGEX, "").trim();
+ // Split on multiple spaces and take the first part as slug, rest as name
+ const [slug, ...nameParts] = cleanLine.split(MULTI_SPACE_REGEX);
+ if (!slug || nameParts.length === 0) {
+ return null;
+ }
+
+ return {
+ slug,
+ name: nameParts.join(" ").trim(),
+ };
+ })
+ .filter((team): team is { slug: string; name: string } => team !== null);
+
+ if (state.debug) {
+ console.log("\nDebug: Parsed teams:", teams);
+ }
+
+ return teams;
+ } catch (error) {
+ if (state.debug) {
+ console.error("Error getting Vercel teams:", error);
+ }
+ return [];
+ }
+}
+
+async function setupVercelProject() {
+ const spinner = p.spinner();
+
+ try {
+ // Get project name from package.json
+ const pkgJson = (await fs.readJSON("package.json")) as PackageJson;
+ const projectName = pkgJson.name;
+
+ // Get available teams
+ const teams = await getVercelTeams();
+
+ let teamFlag = "";
+ if (teams.length > 1) {
+ const teamChoice = await p.select({
+ message: "Select a team to deploy under:",
+ options: [
+ ...teams.map((team) => ({
+ value: team.slug,
+ label: team.name,
+ })),
+ ],
+ });
+
+ if (p.isCancel(teamChoice)) {
+ console.log(chalk.yellow("\nOperation cancelled"));
+ return false;
+ }
+
+ if (teamChoice && typeof teamChoice === "string") {
+ teamFlag = `--scope=${teamChoice}`;
+ }
+ }
+
+ spinner.start("Creating Vercel project...");
+
+ // Create project with default settings
+ await execa("vercel", ["link", "--yes", ...(teamFlag ? [teamFlag] : [])], {
+ env: {
+ VERCEL_PROJECT_NAME: projectName,
+ },
+ });
+
+ // Pull project settings
+ spinner.message("Pulling project settings...");
+ await execa("vercel", ["pull", "--yes"], {
+ stdio: "inherit",
+ });
+
+ spinner.stop("Vercel project created successfully");
+ return true;
+ } catch (error) {
+ spinner.stop("Failed to set up Vercel project");
+ console.error(chalk.red("Error setting up Vercel project:"), error);
+ return false;
+ }
+}
+
+async function pushEnvironmentVariables() {
+ const spinner = p.spinner();
+ spinner.start("Pushing environment variables to Vercel...");
+
+ try {
+ const settings = getSettings();
+ const envFile = path.join(process.cwd(), settings.envFile ?? ".env");
+
+ if (!fs.existsSync(envFile)) {
+ spinner.stop("No environment file found");
+ return true;
+ }
+
+ const envContent = await fs.readFile(envFile, "utf-8");
+ const envVars = envContent
+ .split("\n")
+ .filter((line) => line.trim() && !line.startsWith("#"))
+ .map((line) => {
+ const [key, ...valueParts] = line.split("=");
+ if (!key) {
+ return null;
+ }
+ const value = valueParts.join("="); // Rejoin in case value contains =
+ return { key: key.trim(), value: value.trim() };
+ })
+ .filter((item): item is { key: string; value: string } => item !== null);
+
+ if (state.debug) {
+ spinner.stop();
+ console.log("\nDebug: Parsed environment variables:");
+ for (const { key, value } of envVars) {
+ console.log(` ${key}=${value.substring(0, 3)}...`);
+ }
+ spinner.start("Pushing environment variables to Vercel...");
+ }
+
+ let failed = 0;
+ const total = envVars.length;
+
+ for (let i = 0; i < total; i++) {
+ const envVar = envVars[i];
+ if (!envVar) {
+ continue;
+ }
+ const { key, value } = envVar;
+ spinner.message(`Pushing environment variables to Vercel... (${i + 1}/${total})`);
+
+ try {
+ if (state.debug) {
+ console.log(`\nDebug: Attempting to add ${key} to Vercel...`);
+ }
+
+ const result = await execa("vercel", ["env", "add", key, "production"], {
+ input: value,
+ stdio: "pipe",
+ reject: false,
+ });
+
+ if (state.debug) {
+ console.log(`Debug: Command exit code: ${result.exitCode}`);
+ if (result.stdout) {
+ console.log("Debug: stdout:", result.stdout);
+ }
+ if (result.stderr) {
+ console.log("Debug: stderr:", result.stderr);
+ }
+ }
+
+ if (result.exitCode !== 0) {
+ throw new Error(`Command failed with exit code ${result.exitCode}`);
+ }
+ } catch (error) {
+ failed++;
+ if (state.debug) {
+ console.error(chalk.yellow(`\nDebug: Failed to add ${key}`));
+ console.error("Debug: Full error:", error);
+ }
+ }
+ }
+
+ if (failed > 0) {
+ spinner.stop(chalk.yellow(`Environment variables pushed with ${failed} failures`));
+ } else {
+ spinner.stop("Environment variables pushed successfully");
+ }
+ return failed < total;
+ } catch (error) {
+ spinner.stop("Failed to push environment variables");
+ if (state.debug) {
+ console.error("\nDebug: Top-level error in pushEnvironmentVariables:");
+ console.error(error);
+ }
+ return false;
+ }
+}
+
+interface VercelProjectConfig {
+ projectId: string;
+ settings?: {
+ nodeVersion?: string;
+ };
+ [key: string]: unknown;
+}
+
+async function ensureCorrectNodeVersion() {
+ const nodeVersion = process.version.replace(VERSION_PREFIX_REGEX, "");
+ const majorVersion = nodeVersion.split(".")[0];
+
+ try {
+ const projectJsonPath = ".vercel/project.json";
+ if (!fs.existsSync(projectJsonPath)) {
+ if (state.debug) {
+ console.log("Debug: No project.json found");
+ }
+ return false;
+ }
+
+ const projectConfig = (await fs.readJSON(projectJsonPath)) as VercelProjectConfig;
+ if (state.debug) {
+ console.log("Debug: Current project config:", projectConfig);
+ }
+
+ // Update the Node.js version
+ projectConfig.settings = {
+ ...projectConfig.settings,
+ nodeVersion: `${majorVersion}.x`,
+ };
+
+ await fs.writeJSON(projectJsonPath, projectConfig, { spaces: 2 });
+ if (state.debug) {
+ console.log(`Debug: Updated Node.js version to ${majorVersion}.x`);
+ }
+ return true;
+ } catch (error) {
+ if (state.debug) {
+ console.error("Debug: Failed to update Node.js version:", error);
+ }
+ return false;
+ }
+}
+
+async function checkVercelLogin(): Promise {
+ try {
+ const result = await execa("vercel", ["whoami"], {
+ stdio: "pipe",
+ reject: false,
+ });
+
+ if (state.debug) {
+ console.log("\nDebug: Vercel whoami result:", result);
+ }
+
+ return result.exitCode === 0;
+ } catch (error) {
+ if (state.debug) {
+ console.error("Debug: Error checking Vercel login status:", error);
+ }
+ return false;
+ }
+}
+
+async function loginToVercel(): Promise {
+ console.log(chalk.blue("\nYou need to log in to Vercel first."));
+
+ try {
+ await execa("vercel", ["login"], {
+ stdio: "inherit",
+ });
+ return true;
+ } catch (error) {
+ console.error(chalk.red("\nFailed to log in to Vercel:"), error);
+ return false;
+ }
+}
+
+export async function runDeploy() {
+ if (state.debug) {
+ console.log("Running deploy...");
+ }
+
+ // Check if Vercel CLI is installed
+ const hasVercelCLI = await checkVercelCLI();
+
+ if (!hasVercelCLI) {
+ const installed = await installVercelCLI();
+ if (!installed) {
+ console.log(chalk.red("\nFailed to install Vercel CLI. Please install it manually using:"));
+ console.log(chalk.blue("\n npm install -g vercel"));
+ return;
+ }
+ }
+
+ // Check if user is logged in
+ const isLoggedIn = await checkVercelLogin();
+ if (!isLoggedIn) {
+ const loginSuccessful = await loginToVercel();
+ if (!loginSuccessful) {
+ console.log(chalk.red("\nFailed to log in to Vercel. Please try again."));
+ return;
+ }
+ }
+
+ // Check if project is set up with Vercel
+ const hasVercelProject = await checkVercelProject();
+
+ if (!hasVercelProject) {
+ console.log(chalk.blue("\nSetting up new Vercel project..."));
+ const setup = await setupVercelProject();
+ if (!setup) {
+ console.log(chalk.red("\nFailed to set up Vercel project automatically."));
+ return;
+ }
+
+ const envPushed = await pushEnvironmentVariables();
+ if (!envPushed) {
+ console.log(chalk.red("\nFailed to push environment variables. Aborting deployment."));
+ return;
+ }
+ }
+
+ // Pull latest project settings
+ console.log(chalk.blue("\nPulling latest project settings..."));
+ try {
+ await execa("vercel", ["pull", "--yes"], {
+ stdio: "inherit",
+ });
+ } catch (error) {
+ console.error(chalk.red("\nFailed to pull project settings:"), error);
+ return;
+ }
+
+ // Ensure correct Node.js version is set
+ if (!(await ensureCorrectNodeVersion())) {
+ console.error(chalk.red("\nFailed to set Node.js version. Continuing anyway..."));
+ }
+
+ if (state.localBuild) {
+ // Build locally for Vercel
+ console.log(chalk.blue("\nPreparing local build for Vercel..."));
+ try {
+ const result = await execa("vercel", ["build"], {
+ stdio: "inherit",
+ reject: false,
+ });
+ if (result.exitCode === 0) {
+ console.log(chalk.green("\n✓ Local build successful!"));
+ } else {
+ console.error(chalk.red("\n✖ Local build failed"));
+ console.log(chalk.yellow("Fix the errors above and then try again."));
+ return;
+ }
+ } catch (error) {
+ console.error(chalk.red("\nVercel build failed:"), error);
+ return;
+ }
+
+ // Deploy the pre-built project
+ console.log(chalk.blue("\nDeploying to Vercel..."));
+
+ const result = await execa("vercel", ["deploy", "--prebuilt", "--yes"], {
+ stdio: "inherit",
+ reject: false,
+ });
+ if (result.exitCode === 0) {
+ console.log(chalk.green("\n✓ Deployment successful!"));
+ }
+ } else {
+ // Deploy and build on Vercel
+ console.log(chalk.blue("\nDeploying to Vercel..."));
+ try {
+ const result = await execa("vercel", ["deploy", "--yes"], {
+ stdio: "inherit",
+ reject: false,
+ });
+ if (result.exitCode === 0) {
+ console.log(chalk.green("\n✓ Deployment successful!"));
+ } else {
+ const pkgManager = getUserPkgManager();
+ const runCmd = pkgManager === "npm" ? "npm run" : pkgManager;
+ console.error(chalk.red("\n✖ Deployment failed"));
+
+ console.log(chalk.yellow("\nTroubleshooting Tips:"));
+ console.log(chalk.dim("You can check for most errors before deploying for a faster iteration cycle"));
+ console.log(
+ `${chalk.dim("Run")} ${runCmd} tsc ${chalk.dim("to check for TypeScript errors (most common build errors)")}`,
+ );
+ console.log(`${chalk.dim("Run")} ${runCmd} build ${chalk.dim("to run the full production build locally")}`);
+ }
+ } catch {
+ // This catch block should rarely be hit since we're using reject: false
+ return;
+ }
+ }
+}
+
+export const makeDeployCommand = () => {
+ const deployCommand = new Command("deploy")
+ .description("Deploy your ProofKit application to Vercel")
+ .addOption(ciOption)
+ .addOption(debugOption)
+ .addOption(new Option("--local-build", "Build locally before deploying"))
+ .action(runDeploy);
+
+ deployCommand.hook("preAction", (thisCommand) => {
+ initProgramState(thisCommand.opts());
+ state.baseCommand = "deploy";
+ ensureProofKitProject({ commandName: "deploy" });
+ });
+
+ return deployCommand;
+};
diff --git a/packages/cli-old/src/cli/fmdapi.ts b/packages/cli-old/src/cli/fmdapi.ts
new file mode 100644
index 00000000..cb252b8a
--- /dev/null
+++ b/packages/cli-old/src/cli/fmdapi.ts
@@ -0,0 +1,57 @@
+import DataApi, { type clientTypes, OttoAdapter, type OttoAPIKey } from "@proofkit/fmdapi";
+
+export async function getLayouts({
+ dataApiKey,
+ fmFile,
+ server,
+}: {
+ dataApiKey: OttoAPIKey;
+ fmFile: string;
+ server: string;
+}) {
+ const DapiClient = DataApi({
+ adapter: new OttoAdapter({
+ auth: { apiKey: dataApiKey },
+ db: fmFile,
+ server,
+ }),
+ layout: "",
+ });
+
+ const layoutsResp = await DapiClient.layouts();
+
+ const layouts = transformLayoutList(layoutsResp.layouts);
+
+ return layouts;
+}
+
+function getAllLayoutNames(layout: clientTypes.LayoutOrFolder): string[] {
+ if ("isFolder" in layout) {
+ return (layout.folderLayoutNames ?? []).flatMap(getAllLayoutNames);
+ }
+ return [layout.name];
+}
+
+export const commonFileMakerLayoutPrefixes = ["API_", "API ", "dapi_", "dapi"];
+
+export function transformLayoutList(layouts: clientTypes.LayoutOrFolder[]): string[] {
+ const flatList = layouts.flatMap(getAllLayoutNames);
+
+ // sort the list so that any values that begin with one of the prefixes are at the top
+
+ const sortedList = flatList.sort((a, b) => {
+ const aPrefix = commonFileMakerLayoutPrefixes.find((prefix) => a.startsWith(prefix));
+ const bPrefix = commonFileMakerLayoutPrefixes.find((prefix) => b.startsWith(prefix));
+ if (aPrefix && bPrefix) {
+ return a.localeCompare(b);
+ }
+ if (aPrefix) {
+ return -1;
+ }
+ if (bPrefix) {
+ return 1;
+ }
+ return a.localeCompare(b);
+ });
+ return sortedList;
+}
diff --git a/packages/cli-old/src/cli/init.ts b/packages/cli-old/src/cli/init.ts
new file mode 100644
index 00000000..4b8cc21c
--- /dev/null
+++ b/packages/cli-old/src/cli/init.ts
@@ -0,0 +1,395 @@
+import path from "node:path";
+import { Command } from "commander";
+import { execa } from "execa";
+import fs from "fs-extra";
+import type { PackageJson } from "type-fest";
+
+import { DEFAULT_APP_NAME } from "~/consts.js";
+import { addAuth } from "~/generators/auth.js";
+import { runCodegenCommand } from "~/generators/fmdapi.js";
+import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js";
+import { createBareProject } from "~/helpers/createProject.js";
+import { initializeGit } from "~/helpers/git.js";
+import { installDependencies } from "~/helpers/installDependencies.js";
+import { logNextSteps } from "~/helpers/logNextSteps.js";
+import { setImportAlias } from "~/helpers/setImportAlias.js";
+import { buildPkgInstallerMap } from "~/installers/index.js";
+import { initProgramState, isNonInteractiveMode, state } from "~/state.js";
+import { getVersion } from "~/utils/getProofKitVersion.js";
+import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
+import { parseNameAndPath } from "~/utils/parseNameAndPath.js";
+import { type Settings, setSettings } from "~/utils/parseSettings.js";
+import { validateAppName } from "~/utils/validateAppName.js";
+import { promptForFileMakerDataSource } from "./add/data-source/filemaker.js";
+import { select, text } from "./prompts.js";
+import { abortIfCancel } from "./utils.js";
+
+interface CliFlags {
+ noGit: boolean;
+ noInstall: boolean;
+ force: boolean;
+ default: boolean;
+ importAlias: string;
+ server?: string;
+ adminApiKey?: string;
+ fileName: string;
+ layoutName: string;
+ schemaName: string;
+ dataApiKey: string;
+ fmServerURL: string;
+ auth: "none" | "next-auth" | "clerk";
+ dataSource?: "filemaker" | "none" | "supabase";
+ /** @internal UI library selection; hidden flag */
+ ui?: "shadcn" | "mantine";
+ /** @internal Used in CI. */
+ CI: boolean;
+ /** @internal Used in non-interactive mode. */
+ nonInteractive?: boolean;
+ /** @internal Used in CI. */
+ tailwind: boolean;
+ /** @internal Used in CI. */
+ trpc: boolean;
+ /** @internal Used in CI. */
+ prisma: boolean;
+ /** @internal Used in CI. */
+ drizzle: boolean;
+ /** @internal Used in CI. */
+ appRouter: boolean;
+}
+
+const defaultOptions: CliFlags = {
+ noGit: false,
+ noInstall: false,
+ force: false,
+ default: false,
+ CI: false,
+ tailwind: false,
+ trpc: false,
+ prisma: false,
+ drizzle: false,
+ importAlias: "~/",
+ appRouter: false,
+ auth: "none",
+ server: undefined,
+ fileName: "",
+ layoutName: "",
+ schemaName: "",
+ dataApiKey: "",
+ fmServerURL: "",
+ dataSource: undefined,
+ ui: "shadcn",
+};
+
+export const makeInitCommand = () => {
+ const initCommand = new Command("init")
+ .description("Create a new project with ProofKit")
+ .argument("[dir]", "The name of the application, as well as the name of the directory to create")
+ .option("--appType [type]", "The type of app to create", undefined)
+ // hidden UI selector; default is shadcn; pass --ui mantine to opt-in legacy Mantine templates
+ .option("--ui [ui]", undefined, undefined)
+ .option("--server [url]", "The URL of your FileMaker Server", undefined)
+ .option("--adminApiKey [key]", "Admin API key for OttoFMS. If provided, will skip login prompt", undefined)
+ .option("--fileName [name]", "The name of the FileMaker file to use for the web app", undefined)
+ .option("--layoutName [name]", "The name of the FileMaker layout to use for the web app", undefined)
+ .option("--schemaName [name]", "The name for the generated layout client in your schemas", undefined)
+ .option("--dataApiKey [key]", "The API key to use for the FileMaker Data API", undefined)
+ .option("--auth [type]", "The authentication provider to use for the web app", undefined)
+ .option("--dataSource [type]", "The data source to use for the web app (filemaker or none)", undefined)
+ .option("--noGit", "Explicitly tell the CLI to not initialize a new git repo in the project", false)
+ .option("--noInstall", "Explicitly tell the CLI to not run the package manager's install command", false)
+ .option("-f, --force", "Force overwrite target directory when it already contains files", false)
+ .addOption(ciOption)
+ .addOption(nonInteractiveOption)
+ .addOption(debugOption)
+ .action(runInit);
+
+ initCommand.hook("preAction", (cmd) => {
+ initProgramState(cmd.opts());
+ state.baseCommand = "init";
+ });
+
+ return initCommand;
+};
+
+async function askForAuth({ projectDir }: { projectDir: string }) {
+ const authType = "none" as "none" | "clerk" | "fmaddon";
+ if (authType === "clerk") {
+ await addAuth({
+ options: { type: "clerk" },
+ projectDir,
+ noInstall: true,
+ });
+ } else if (authType === "fmaddon") {
+ await addAuth({
+ options: { type: "fmaddon" },
+ projectDir,
+ noInstall: true,
+ });
+ }
+}
+
+type ProofKitPackageJSON = PackageJson & {
+ proofkitMetadata?: {
+ initVersion: string;
+ };
+};
+
+const missingTypegenCommandPatterns = [
+ /ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL[\s\S]*Command\s+["'`]typegen["'`]\s+not found/i,
+ /Command\s+["'`]typegen["'`]\s+not found/i,
+ /Missing script:\s*["'`]typegen["'`]/i,
+ /Script not found\s*["'`]typegen["'`]/i,
+];
+
+function getErrorMessage(error: unknown): string {
+ if (error instanceof Error) {
+ return error.message;
+ }
+ return String(error);
+}
+
+export function isMissingTypegenCommandError(error: unknown): boolean {
+ const message = getErrorMessage(error);
+ return missingTypegenCommandPatterns.some((pattern) => pattern.test(message));
+}
+
+export function createPostInitGenerationError({
+ error,
+ appType,
+ projectDir,
+}: {
+ error: unknown;
+ appType: "browser" | "webviewer";
+ projectDir: string;
+}) {
+ const rootError = error instanceof Error ? error : new Error(getErrorMessage(error));
+
+ if (appType === "browser" && isMissingTypegenCommandError(error)) {
+ return new Error(
+ [
+ "Post-init generation failed after scaffolding.",
+ `Project created at: ${projectDir}`,
+ "Root cause: a `typegen` package command was invoked, but browser scaffolds do not define that script.",
+ "Continue using the generated project, then run `proofkit typegen` later after FileMaker setup is complete.",
+ ].join("\n"),
+ { cause: rootError },
+ );
+ }
+
+ return new Error(
+ [
+ "Post-init generation failed after scaffolding.",
+ `Project created at: ${projectDir}`,
+ "Retry `proofkit typegen` from inside the project once FileMaker settings and connectivity are valid.",
+ `Underlying error: ${getErrorMessage(error)}`,
+ ].join("\n"),
+ { cause: rootError },
+ );
+}
+
+export const runInit = async (name?: string, opts?: CliFlags) => {
+ const pkgManager = getUserPkgManager();
+ const cliOptions = opts ?? defaultOptions;
+ const nonInteractive = isNonInteractiveMode();
+ const noInstall = cliOptions.noInstall ?? (opts as { install?: boolean } | undefined)?.install === false;
+ const noGit = cliOptions.noGit ?? (opts as { git?: boolean } | undefined)?.git === false;
+ // capture ui choice early into state
+ state.ui = (cliOptions.ui ?? "shadcn") as "shadcn" | "mantine";
+
+ let projectName = name;
+ if (!projectName) {
+ if (nonInteractive) {
+ throw new Error("Project name is required in non-interactive mode.");
+ }
+ projectName = abortIfCancel(
+ await text({
+ message: "What will your project be called?",
+ defaultValue: DEFAULT_APP_NAME,
+ validate: validateAppName,
+ }),
+ ).toString();
+ }
+
+ const appNameValidation = validateAppName(projectName);
+ if (appNameValidation) {
+ throw new Error(appNameValidation);
+ }
+
+ const hasExplicitFileMakerInputs = Boolean(
+ cliOptions.server ||
+ cliOptions.adminApiKey ||
+ cliOptions.dataApiKey ||
+ cliOptions.fileName ||
+ cliOptions.layoutName ||
+ cliOptions.schemaName,
+ );
+ const hasPartialFileMakerSchemaInputs = Boolean(cliOptions.layoutName) !== Boolean(cliOptions.schemaName);
+
+ if (!state.appType) {
+ state.appType = nonInteractive
+ ? "browser"
+ : (abortIfCancel(
+ await select({
+ message: "What kind of app do you want to build?",
+ options: [
+ {
+ value: "browser",
+ label: "Web App for Browsers",
+ hint: "Uses Next.js, will require hosting",
+ },
+ {
+ value: "webviewer",
+ label: "FileMaker Web Viewer (beta)",
+ hint: "Uses Vite, can be embedded in FileMaker or hosted",
+ },
+ ],
+ }),
+ ) as "browser" | "webviewer");
+ }
+
+ if (nonInteractive && hasPartialFileMakerSchemaInputs) {
+ throw new Error("Both --layoutName and --schemaName must be provided together.");
+ }
+
+ if (nonInteractive && hasExplicitFileMakerInputs) {
+ const resolvedDataSourceForValidation =
+ state.appType === "webviewer"
+ ? (cliOptions.dataSource ?? (cliOptions.server ? "filemaker" : "none"))
+ : (cliOptions.dataSource ?? "none");
+
+ if (resolvedDataSourceForValidation !== "filemaker") {
+ throw new Error("FileMaker flags require --dataSource filemaker in non-interactive mode.");
+ }
+ }
+
+ const usePackages = buildPkgInstallerMap();
+
+ // e.g. dir/@mono/app returns ["@mono/app", "dir/app"]
+ const [scopedAppName, appDir] = parseNameAndPath(projectName);
+
+ const projectDir = await createBareProject({
+ projectName: appDir,
+ scopedAppName,
+ packages: usePackages,
+ noInstall,
+ force: cliOptions.force,
+ appRouter: cliOptions.appRouter,
+ });
+ setImportAlias(projectDir, "@/");
+
+ // Write name to package.json
+ const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as ProofKitPackageJSON;
+ pkgJson.name = scopedAppName;
+ pkgJson.proofkitMetadata = { initVersion: getVersion() };
+
+ // ? Bun doesn't support this field (yet)
+ if (pkgManager !== "bun") {
+ const { stdout } = await execa(pkgManager, ["-v"], {
+ cwd: projectDir,
+ });
+ pkgJson.packageManager = `${pkgManager}@${stdout.trim()}`;
+ }
+
+ fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, {
+ spaces: 2,
+ });
+
+ // Ensure proofkit.json exists with initial settings including ui
+ const initialSettings: Settings =
+ state.ui === "mantine"
+ ? {
+ appType: state.appType ?? "browser",
+ ui: "mantine",
+ auth: { type: "none" },
+ envFile: ".env",
+ dataSources: [],
+ tanstackQuery: false,
+ replacedMainPage: false,
+ appliedUpgrades: [],
+ reactEmail: false,
+ reactEmailServer: false,
+ registryTemplates: [],
+ }
+ : {
+ appType: state.appType ?? "browser",
+ ui: "shadcn",
+ envFile: ".env",
+ dataSources: [],
+ replacedMainPage: false,
+ registryTemplates: [],
+ };
+ setSettings(initialSettings);
+
+ // for webviewer apps FM is required, so don't ask
+ let dataSource =
+ state.appType === "webviewer"
+ ? (cliOptions.dataSource ?? (nonInteractive && !cliOptions.server ? "none" : "filemaker"))
+ : (cliOptions.dataSource ?? (nonInteractive ? "none" : undefined));
+ if (!dataSource) {
+ dataSource = abortIfCancel(
+ await select({
+ message: "Do you want to connect to a FileMaker Database now?",
+ options: [
+ {
+ value: "filemaker",
+ label: "Yes",
+ hint: "Requires OttoFMS and Admin Server credentials",
+ },
+ // { value: "supabase", label: "Supabase" },
+ {
+ value: "none",
+ label: "No",
+ hint: "You'll be able to add a new data source later",
+ },
+ ],
+ }),
+ ) as "filemaker" | "none" | "supabase";
+ }
+
+ if (dataSource === "filemaker") {
+ // later will split this flow to ask for which kind of data souce, but for now it's just FM
+ await promptForFileMakerDataSource({
+ projectDir,
+ name: "filemaker",
+ adminApiKey: cliOptions.adminApiKey,
+ dataApiKey: cliOptions.dataApiKey,
+ server: cliOptions.server,
+ fileName: cliOptions.fileName,
+ layoutName: cliOptions.layoutName,
+ schemaName: cliOptions.schemaName,
+ });
+ } else if (dataSource === "supabase") {
+ // TODO: add supabase
+ }
+
+ await askForAuth({ projectDir });
+
+ if (!noInstall) {
+ await installDependencies({ projectDir });
+ }
+
+ if (dataSource === "filemaker") {
+ const shouldRunInitialCodegen = state.appType === "webviewer" && !(nonInteractive && !hasExplicitFileMakerInputs);
+
+ if (shouldRunInitialCodegen) {
+ try {
+ await runCodegenCommand();
+ } catch (error) {
+ throw createPostInitGenerationError({
+ error,
+ appType: state.appType ?? "browser",
+ projectDir,
+ });
+ }
+ }
+ }
+
+ if (!noGit) {
+ await initializeGit(projectDir);
+ }
+
+ logNextSteps({
+ projectName: appDir,
+ noInstall,
+ });
+};
diff --git a/packages/cli-old/src/cli/menu.ts b/packages/cli-old/src/cli/menu.ts
new file mode 100644
index 00000000..be40d6a7
--- /dev/null
+++ b/packages/cli-old/src/cli/menu.ts
@@ -0,0 +1,102 @@
+import chalk from "chalk";
+import open from "open";
+import { confirm, log, select } from "~/cli/prompts.js";
+
+import { DOCS_URL } from "~/consts.js";
+import { checkForAvailableUpgrades, runAllAvailableUpgrades } from "~/upgrades/index.js";
+import { getSettings } from "~/utils/parseSettings.js";
+import { runAdd } from "./add/index.js";
+import { runDeploy } from "./deploy/index.js";
+import { runRemove } from "./remove/index.js";
+import { runTypegen } from "./typegen/index.js";
+import { runUpgrade } from "./update/index.js";
+import { abortIfCancel } from "./utils.js";
+
+export const runMenu = async () => {
+ const settings = getSettings();
+ const upgrades = checkForAvailableUpgrades();
+
+ if (upgrades.length > 0) {
+ log.info(
+ `${chalk.yellow("There are upgrades available for your ProofKit project")}\n${upgrades
+ .map((upgrade) => `- ${upgrade.title}`)
+ .join("\n")}`,
+ );
+
+ const shouldRunUpgrades = abortIfCancel(
+ await confirm({
+ message: "Would you like to run them now?",
+ initialValue: true,
+ }),
+ );
+
+ if (shouldRunUpgrades) {
+ await runAllAvailableUpgrades();
+ log.success(chalk.green("Successfully ran all upgrades"));
+ } else {
+ log.info(`You can apply the upgrades later by running ${chalk.cyan("proofkit upgrade")}`);
+ }
+ }
+
+ const menuChoice = abortIfCancel(
+ await select({
+ message: "What would you like to do?",
+ options: [
+ {
+ label: "Add Components",
+ value: "add",
+ hint: "Add new pages, schemas, data sources, etc.",
+ },
+ {
+ label: "Remove Components",
+ value: "remove",
+ hint: "Remove pages, schemas, data sources, etc.",
+ },
+ {
+ label: "Generate Types",
+ value: "typegen",
+ hint: "Update field definitions from your data sources",
+ },
+ {
+ label: "Deploy",
+ value: "deploy",
+ hint: "Deploy your app to Vercel",
+ },
+ {
+ label: "Upgrade Components",
+ value: "upgrade",
+ hint: "Update ProofKit components to latest version",
+ },
+ {
+ label: "View Documentation",
+ value: "docs",
+ hint: "Open ProofKit documentation",
+ },
+ ],
+ }),
+ );
+
+ switch (menuChoice) {
+ case "add":
+ await runAdd(undefined);
+ break;
+ case "remove":
+ await runRemove(undefined);
+ break;
+ case "docs":
+ log.info(`Opening ${chalk.cyan(DOCS_URL)} in your browser...`);
+ await open(DOCS_URL);
+ break;
+ case "typegen":
+ await runTypegen({ settings });
+ break;
+ case "deploy":
+ await runDeploy();
+ break;
+ case "upgrade":
+ await runUpgrade();
+ break;
+ default:
+ throw new Error(`Unknown menu choice: ${menuChoice}`);
+ }
+};
diff --git a/packages/cli-old/src/cli/ottofms.ts b/packages/cli-old/src/cli/ottofms.ts
new file mode 100644
index 00000000..569ad348
--- /dev/null
+++ b/packages/cli-old/src/cli/ottofms.ts
@@ -0,0 +1,268 @@
+import axios, { AxiosError } from "axios";
+import chalk from "chalk";
+import open from "open";
+import randomstring from "randomstring";
+import { z } from "zod/v4";
+
+import * as clack from "~/cli/prompts.js";
+import { abortIfCancel } from "./utils.js";
+
+interface WizardResponse {
+ token: string;
+}
+export async function getOttoFMSToken({ url }: { url: URL }): Promise<{ token: string }> {
+ // generate a random string
+ const hash = randomstring.generate({ length: 18, charset: "alphanumeric" });
+
+ const loginUrl = new URL(`/otto/wizard/${hash}`, url.origin);
+
+ const urlToOpen = loginUrl.toString();
+ clack.log.info(
+ `${chalk.bold(
+ `If the browser window didn't open automatically, please open the following link to login into your OttoFMS server:`,
+ )}\n\n${chalk.cyan(urlToOpen)}`,
+ );
+
+ open(loginUrl.toString()).catch(() => {
+ // Ignore errors from open() - the user can manually open the URL
+ });
+
+ const loginSpinner = clack.spinner();
+
+ loginSpinner.start("Waiting for you to log in using the link above");
+
+ const data = await new Promise((resolve) => {
+ const pollingInterval = setInterval(() => {
+ axios
+ .get<{ response: WizardResponse }>(`${url.origin}/otto/api/cli/checkHash/${hash}`, {
+ headers: {
+ "Accept-Encoding": "deflate",
+ },
+ })
+ .then((result) => {
+ resolve(result.data.response);
+ clearTimeout(timeout);
+ clearInterval(pollingInterval);
+ axios
+ .delete(`${url.origin}/otto/api/cli/checkHash/${hash}`, {
+ headers: {
+ "Accept-Encoding": "deflate",
+ },
+ })
+ .catch(() => {
+ // Ignore cleanup errors
+ });
+ })
+ .catch(() => {
+ // noop - just try again
+ });
+ }, 500);
+
+ const timeout = setTimeout(() => {
+ clearInterval(pollingInterval);
+ loginSpinner.stop("Login timed out. No worries - it happens to the best of us.");
+ }, 180_000); // 3 minutes
+ });
+ // clack.log.info(`Token: ${JSON.stringify(data)}`);
+
+ loginSpinner.stop("Login complete.");
+
+ return data;
+}
+
+interface ListFilesResponse {
+ response: {
+ databases: {
+ clients: number;
+ decryptHint: string;
+ enabledExtPrivileges: string[];
+ filename: string;
+ folder: string;
+ hasSavedDecryptKey: boolean;
+ id: string;
+ isEncrypted: boolean;
+ size: number;
+ status: string;
+ }[];
+ };
+}
+
+export async function listFiles({ url, token }: { url: URL; token: string }) {
+ const response = await axios.get(`${url.origin}/otto/fmi/admin/api/v2/databases`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data.response.databases;
+}
+
+interface ListAPIKeysResponse {
+ response: {
+ "api-keys": {
+ id: number;
+ key: string;
+ token: string;
+ user: string;
+ database: string;
+ label: string;
+ created_at: string;
+ updated_at: string;
+ }[];
+ };
+}
+
+export async function listAPIKeys({ url, token }: { url: URL; token: string }) {
+ const response = await axios.get(`${url.origin}/otto/api/api-key`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data.response["api-keys"];
+}
+
+interface CreateAPIKeyResponse {
+ response: {
+ key: string;
+ token: string;
+ };
+}
+export async function createDataAPIKey({ url, filename }: { url: URL; filename: string }) {
+ clack.log.info(
+ `${chalk.cyan("Creating a Data API Key")}\nEnter FileMaker credentials for ${chalk.bold(filename)}.\n${chalk.dim("The account must have the fmrest extended privilege enabled.")}`,
+ );
+
+ while (true) {
+ const username = abortIfCancel(
+ await clack.text({
+ message: `Enter the account name for ${chalk.bold(filename)}`,
+ }),
+ );
+
+ const password = abortIfCancel(
+ await clack.password({
+ message: `Enter the password for ${chalk.bold(username)}`,
+ }),
+ );
+
+ try {
+ const response = await createDataAPIKeyWithCredentials({
+ url,
+ filename,
+ username,
+ password,
+ });
+
+ return response;
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ const respMsg =
+ error.response?.data && "messages" in error.response.data
+ ? (error.response.data as { messages?: { text?: string }[] }).messages?.[0]?.text
+ : undefined;
+
+ clack.log.error(
+ `${chalk.red("Error creating Data API key:")} ${respMsg ?? `Error code ${error.response?.status}`}
+${chalk.dim(
+ error.response?.status === 400 &&
+ `Common reasons this might happen:
+- The provided credentials are incorrect.
+- The account does not have the fmrest extended privilege enabled.
+
+You may also want to try to create an API directly in the OttoFMS dashboard:
+${url.origin}/otto/app/api-keys`,
+)}
+ `,
+ );
+ } else {
+ clack.log.error(`${chalk.red("Error creating Data API key:")} Unknown error`);
+ }
+ const tryAgain = abortIfCancel(
+ await clack.confirm({
+ message: "Do you want to try and enter credentials again?",
+ active: "Yes, try again",
+ inactive: "No, abort",
+ }),
+ );
+ if (!tryAgain) {
+ throw new Error("User cancelled");
+ }
+ }
+ }
+}
+
+export async function createDataAPIKeyWithCredentials({
+ url,
+ filename,
+ username,
+ password,
+}: {
+ url: URL;
+ filename: string;
+ username: string;
+ password: string;
+}) {
+ const response = await axios.post(`${url.origin}/otto/api/api-key/create-only`, {
+ database: filename,
+ label: "For FM Web App",
+ user: username,
+ pass: password,
+ });
+
+ return { apiKey: response.data.response.key };
+}
+
+export async function startDeployment({ payload, url, token }: { payload: unknown; url: URL; token: string }) {
+ const responseSchema = z.object({
+ response: z.object({
+ started: z.boolean(),
+ batchId: z.number(),
+ subDeploymentIds: z.array(z.number()),
+ }),
+ messages: z.array(z.object({ code: z.number(), text: z.string() })),
+ });
+
+ const response = await axios
+ .post(`${url.origin}/otto/api/deployment`, payload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ })
+ .catch((error) => {
+ console.error(error.response.data);
+ throw error;
+ });
+
+ return responseSchema.parse(response.data);
+}
+
+export async function getDeploymentStatus({
+ url,
+ token,
+ deploymentId,
+}: {
+ url: URL;
+ token: string;
+ deploymentId: number;
+}) {
+ const schema = z.object({
+ response: z.object({
+ id: z.number(),
+ status: z.enum(["queued", "running", "scheduled", "complete", "aborted", "unknown"]),
+ running: z.coerce.boolean(),
+ created_at: z.string(),
+ started_at: z.string(),
+ updated_at: z.string(),
+ }),
+ messages: z.array(z.object({ code: z.number(), text: z.string() })),
+ });
+
+ const response = await axios.get(`${url.origin}/otto/api/deployment/${deploymentId}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return schema.parse(response.data);
+}
diff --git a/packages/cli-old/src/cli/prompts.ts b/packages/cli-old/src/cli/prompts.ts
new file mode 100644
index 00000000..c4b7900e
--- /dev/null
+++ b/packages/cli-old/src/cli/prompts.ts
@@ -0,0 +1,188 @@
+import * as clack from "@clack/prompts";
+import {
+ checkbox as inquirerCheckbox,
+ confirm as inquirerConfirm,
+ input as inquirerInput,
+ password as inquirerPassword,
+ search as inquirerSearch,
+ select as inquirerSelect,
+} from "@inquirer/prompts";
+
+const CANCEL_SYMBOL = Symbol.for("@proofkit/cli/prompt-cancelled");
+
+export const intro = clack.intro;
+export const outro = clack.outro;
+export const note = clack.note;
+export const log = clack.log;
+export const spinner = clack.spinner;
+export const cancel = clack.cancel;
+
+export interface PromptOption {
+ value: T;
+ label: string;
+ hint?: string;
+ disabled?: boolean | string;
+}
+
+export interface SearchPromptOption extends PromptOption {
+ keywords?: readonly string[];
+}
+
+function normalizeValidate(
+ validate: ((value: string) => string | undefined) | undefined,
+): ((value: string) => string | boolean) | undefined {
+ if (!validate) {
+ return undefined;
+ }
+
+ return (value: string) => validate(value) ?? true;
+}
+
+function normalizeDisabledMessage(value: boolean | string | undefined) {
+ if (typeof value === "string") {
+ return value;
+ }
+ return value ? true : undefined;
+}
+
+function isPromptCancel(error: unknown) {
+ return error instanceof Error && error.name === "ExitPromptError";
+}
+
+function withCancelSentinel(fn: () => Promise): Promise {
+ return fn().catch((error: unknown) => {
+ if (isPromptCancel(error)) {
+ return CANCEL_SYMBOL;
+ }
+ throw error;
+ });
+}
+
+export function isCancel(value: unknown): value is symbol {
+ return value === CANCEL_SYMBOL || clack.isCancel(value);
+}
+
+function matchesSearch(option: SearchPromptOption, query: string) {
+ const haystack = [option.label, option.hint ?? "", ...(option.keywords ?? [])].join(" ").toLowerCase();
+ return haystack.includes(query.trim().toLowerCase());
+}
+
+export function filterSearchOptions(
+ options: readonly SearchPromptOption[],
+ query: string | undefined,
+) {
+ const term = query?.trim();
+ if (!term) {
+ return options;
+ }
+
+ return options.filter((option) => matchesSearch(option, term));
+}
+
+export function text(options: {
+ message: string;
+ defaultValue?: string;
+ placeholder?: string;
+ validate?: (value: string) => string | undefined;
+}) {
+ return withCancelSentinel(() =>
+ inquirerInput({
+ message: options.message,
+ default: options.defaultValue,
+ validate: normalizeValidate(options.validate),
+ }),
+ );
+}
+
+export function password(options: { message: string; validate?: (value: string) => string | undefined }) {
+ return withCancelSentinel(() =>
+ inquirerPassword({
+ message: options.message,
+ validate: normalizeValidate(options.validate),
+ }),
+ );
+}
+
+export function confirm(options: { message: string; initialValue?: boolean; active?: string; inactive?: string }) {
+ return withCancelSentinel(
+ () =>
+ inquirerConfirm({
+ message: options.message,
+ default: options.initialValue,
+ }) as Promise,
+ );
+}
+
+export function select(options: {
+ message: string;
+ options: PromptOption[];
+ maxItems?: number;
+ initialValue?: T;
+}) {
+ return withCancelSentinel(() =>
+ inquirerSelect({
+ message: options.message,
+ pageSize: options.maxItems ?? 10,
+ default: options.initialValue,
+ choices: options.options.map((option) => ({
+ value: option.value,
+ name: option.label,
+ description: option.hint,
+ disabled: normalizeDisabledMessage(option.disabled),
+ })),
+ }),
+ );
+}
+
+export function searchSelect(options: {
+ message: string;
+ searchLabel?: string;
+ emptyMessage?: string;
+ options: SearchPromptOption[];
+}) {
+ return withCancelSentinel(() =>
+ inquirerSearch({
+ message: options.message,
+ pageSize: 10,
+ source: (input) => {
+ const filtered = filterSearchOptions(options.options, input);
+ if (filtered.length === 0) {
+ return [
+ {
+ value: "__no_matches__" as T,
+ name: options.emptyMessage ?? "No matches found. Keep typing to refine your search.",
+ disabled: options.emptyMessage ?? "No matches found",
+ },
+ ];
+ }
+
+ return filtered.map((option) => ({
+ value: option.value,
+ name: option.label,
+ description: option.hint,
+ disabled: normalizeDisabledMessage(option.disabled),
+ }));
+ },
+ }),
+ );
+}
+
+export function multiSearchSelect(options: {
+ message: string;
+ options: SearchPromptOption[];
+ required?: boolean;
+}) {
+ return withCancelSentinel(() =>
+ inquirerCheckbox({
+ message: options.message,
+ pageSize: 10,
+ required: options.required,
+ choices: options.options.map((option) => ({
+ value: option.value,
+ name: option.label,
+ description: option.hint,
+ disabled: normalizeDisabledMessage(option.disabled),
+ })),
+ }),
+ );
+}
diff --git a/packages/cli-old/src/cli/react-email.ts b/packages/cli-old/src/cli/react-email.ts
new file mode 100644
index 00000000..f0ac245a
--- /dev/null
+++ b/packages/cli-old/src/cli/react-email.ts
@@ -0,0 +1,27 @@
+import { Command, Option } from "commander";
+import * as p from "~/cli/prompts.js";
+
+import { installReactEmail } from "~/installers/react-email.js";
+
+export const runAddReactEmailCommand = async ({
+ noInstall,
+ installServerFiles,
+}: {
+ noInstall?: boolean;
+ installServerFiles?: boolean;
+} = {}) => {
+ const spinner = p.spinner();
+ spinner.start("Adding React Email");
+ await installReactEmail({ noInstall, installServerFiles });
+ spinner.stop("React Email added");
+};
+
+export const makeAddReactEmailCommand = () => {
+ const addReactEmailCommand = new Command("react-email")
+ .description("Add React Email scaffolding to your project")
+ .addOption(new Option("--noInstall", "Do not run your package manager install command").default(false))
+ .option("--installServerFiles", "Also scaffold provider-specific server email files", false)
+ .action((args: { noInstall?: boolean; installServerFiles?: boolean }) => runAddReactEmailCommand(args));
+
+ return addReactEmailCommand;
+};
diff --git a/packages/cli-old/src/cli/remove/data-source.ts b/packages/cli-old/src/cli/remove/data-source.ts
new file mode 100644
index 00000000..dbc6aebf
--- /dev/null
+++ b/packages/cli-old/src/cli/remove/data-source.ts
@@ -0,0 +1,153 @@
+import path from "node:path";
+import { Command } from "commander";
+import dotenv from "dotenv";
+import fs from "fs-extra";
+import { z } from "zod/v4";
+import * as p from "~/cli/prompts.js";
+
+import { removeFromFmschemaConfig, runCodegenCommand } from "~/generators/fmdapi.js";
+import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js";
+import { initProgramState, isNonInteractiveMode, state } from "~/state.js";
+import { type DataSource, getSettings, setSettings } from "~/utils/parseSettings.js";
+import { abortIfCancel, ensureProofKitProject, UserAbortedError } from "../utils.js";
+
+function getDataSourceInfo(source: DataSource) {
+ if (source.type !== "fm") {
+ return source.type;
+ }
+
+ const envFile = path.join(state.projectDir, ".env");
+ if (fs.existsSync(envFile)) {
+ dotenv.config({ path: envFile });
+ }
+
+ const server = process.env[source.envNames.server] || "unknown server";
+ const database = process.env[source.envNames.database] || "unknown database";
+
+ try {
+ // Format the server URL to be more readable
+ const serverUrl = new URL(server);
+ const formattedServer = serverUrl.hostname;
+ return `${formattedServer}/${database}`;
+ } catch (error) {
+ if (state.debug) {
+ console.error("Error parsing server URL:", error);
+ }
+ return `${server}/${database}`;
+ }
+}
+
+export const runRemoveDataSourceCommand = async (name?: string) => {
+ const settings = getSettings();
+
+ if (settings.dataSources.length === 0) {
+ p.note("No data sources found in your project.");
+ return;
+ }
+
+ let dataSourceName = name;
+
+ // If no name provided, prompt for selection
+ if (dataSourceName) {
+ // Validate that the provided name exists
+ const dataSourceExists = settings.dataSources.some((source) => source.name === dataSourceName);
+ if (!dataSourceExists) {
+ throw new Error(`Data source "${dataSourceName}" not found in your project.`);
+ }
+ } else {
+ dataSourceName = abortIfCancel(
+ await p.select({
+ message: "Which data source do you want to remove?",
+ options: settings.dataSources.map((source) => {
+ let info = "";
+ try {
+ info = getDataSourceInfo(source);
+ } catch (error) {
+ if (state.debug) {
+ console.error("Error getting data source info:", error);
+ }
+ info = "unknown connection";
+ }
+ return {
+ label: `${source.name} (${info})`,
+ value: source.name,
+ };
+ }),
+ }),
+ );
+ }
+
+ let confirmed = true;
+ if (!isNonInteractiveMode()) {
+ confirmed = abortIfCancel(
+ await p.confirm({
+ message: `Are you sure you want to remove the data source "${dataSourceName}"? This will only remove it from your configuration, not replace any possible usage, which may cause TypeScript errors.`,
+ }),
+ );
+
+ if (!confirmed) {
+ throw new UserAbortedError();
+ }
+ }
+
+ // Get the data source before removing it
+ const dataSource = settings.dataSources.find((source) => source.name === dataSourceName);
+
+ // Remove the data source from settings
+ settings.dataSources = settings.dataSources.filter((source) => source.name !== dataSourceName);
+
+ // Save the updated settings
+ setSettings(settings);
+
+ if (dataSource?.type === "fm") {
+ // For FileMaker data sources, remove from fmschema.config.mjs
+ removeFromFmschemaConfig({
+ dataSourceName,
+ });
+
+ if (state.debug) {
+ p.note("Removed schemas from fmschema.config.mjs");
+ }
+
+ // Remove the schema folder for this data source
+ const schemaFolderPath = path.join(state.projectDir, "src", "config", "schemas", dataSourceName);
+ if (fs.existsSync(schemaFolderPath)) {
+ fs.removeSync(schemaFolderPath);
+ if (state.debug) {
+ p.note(`Removed schema folder at ${schemaFolderPath}`);
+ }
+ }
+
+ // Run typegen to regenerate types
+ await runCodegenCommand();
+ if (state.debug) {
+ p.note("Successfully regenerated types");
+ }
+ }
+
+ p.note(`Successfully removed data source "${dataSourceName}"`);
+};
+
+export const makeRemoveDataSourceCommand = () => {
+ const removeDataSourceCommand = new Command("data")
+ .description("Remove a data source from your project")
+ .option("--name ", "Name of the data source to remove")
+ .addOption(ciOption)
+ .addOption(nonInteractiveOption)
+ .addOption(debugOption)
+ .action(async (options) => {
+ const schema = z.object({
+ name: z.string().optional(),
+ });
+ const validated = schema.parse(options);
+ await runRemoveDataSourceCommand(validated.name);
+ });
+
+ removeDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => {
+ initProgramState(actionCommand.opts());
+ state.baseCommand = "remove";
+ ensureProofKitProject({ commandName: "remove" });
+ });
+
+ return removeDataSourceCommand;
+};
diff --git a/packages/cli-old/src/cli/remove/index.ts b/packages/cli-old/src/cli/remove/index.ts
new file mode 100644
index 00000000..954e8765
--- /dev/null
+++ b/packages/cli-old/src/cli/remove/index.ts
@@ -0,0 +1,72 @@
+import { Command } from "commander";
+import * as p from "~/cli/prompts.js";
+
+import { ciOption, debugOption } from "~/globalOptions.js";
+import { initProgramState, state } from "~/state.js";
+import { getSettings } from "~/utils/parseSettings.js";
+import { abortIfCancel, ensureProofKitProject } from "../utils.js";
+import { makeRemoveDataSourceCommand, runRemoveDataSourceCommand } from "./data-source.js";
+import { makeRemovePageCommand, runRemovePageAction } from "./page.js";
+import { makeRemoveSchemaCommand, runRemoveSchemaAction } from "./schema.js";
+
+export const runRemove = async (_name: string | undefined) => {
+ const settings = getSettings();
+
+ const removeType = abortIfCancel(
+ await p.select({
+ message: "What do you want to remove from your project?",
+ options: [
+ { label: "Page", value: "page" },
+ {
+ label: "Schema",
+ value: "schema",
+ hint: "remove a table or layout schema",
+ },
+ ...(settings.appType === "browser"
+ ? [
+ {
+ label: "Data Source",
+ value: "data",
+ hint: "remove a database or FileMaker connection",
+ },
+ ]
+ : []),
+ ],
+ }),
+ );
+
+ if (removeType === "data") {
+ await runRemoveDataSourceCommand();
+ } else if (removeType === "page") {
+ await runRemovePageAction();
+ } else if (removeType === "schema") {
+ await runRemoveSchemaAction();
+ }
+};
+
+export function makeRemoveCommand() {
+ const removeCommand = new Command("remove")
+ .description("Remove a component from your project")
+ .argument("[name]", "Type of component to remove")
+ .addOption(ciOption)
+ .addOption(debugOption)
+ .action(runRemove);
+
+ removeCommand.hook("preAction", (_thisCommand, _actionCommand) => {
+ initProgramState(_actionCommand.opts());
+ state.baseCommand = "remove";
+ ensureProofKitProject({ commandName: "remove" });
+ });
+ removeCommand.hook("preSubcommand", (_thisCommand, _subCommand) => {
+ initProgramState(_subCommand.opts());
+ state.baseCommand = "remove";
+ ensureProofKitProject({ commandName: "remove" });
+ });
+
+ // Add subcommands
+ removeCommand.addCommand(makeRemoveDataSourceCommand());
+ removeCommand.addCommand(makeRemovePageCommand());
+ removeCommand.addCommand(makeRemoveSchemaCommand());
+
+ return removeCommand;
+}
diff --git a/packages/cli-old/src/cli/remove/page.ts b/packages/cli-old/src/cli/remove/page.ts
new file mode 100644
index 00000000..d6574589
--- /dev/null
+++ b/packages/cli-old/src/cli/remove/page.ts
@@ -0,0 +1,214 @@
+import path from "node:path";
+import { Command } from "commander";
+import fs from "fs-extra";
+import { Node, type Project, type PropertyAssignment, SyntaxKind } from "ts-morph";
+import * as p from "~/cli/prompts.js";
+
+import { ciOption, debugOption } from "~/globalOptions.js";
+import { initProgramState, state } from "~/state.js";
+import { getSettings } from "~/utils/parseSettings.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+import { abortIfCancel, ensureProofKitProject } from "../utils.js";
+
+const getExistingRoutes = (project: Project): { label: string; href: string }[] => {
+ const navFilePath = path.join(state.projectDir, "src/app/navigation.tsx");
+
+ // If navigation file doesn't exist (e.g., webviewer apps), there are no nav routes to remove
+ if (!fs.existsSync(navFilePath)) {
+ return [];
+ }
+
+ const sourceFile = project.addSourceFileAtPath(navFilePath);
+
+ const routes: { label: string; href: string }[] = [];
+
+ // Get primary routes
+ const primaryRoutes = sourceFile
+ .getVariableDeclaration("primaryRoutes")
+ ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
+ ?.getElements();
+
+ if (primaryRoutes) {
+ for (const element of primaryRoutes) {
+ if (Node.isObjectLiteralExpression(element)) {
+ const labelProp = element
+ .getProperties()
+ .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "label");
+ const hrefProp = element
+ .getProperties()
+ .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href");
+
+ const label = labelProp?.getInitializer()?.getText().replace(/['"]/g, "");
+ const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, "");
+
+ if (label && href) {
+ routes.push({ label, href });
+ }
+ }
+ }
+ }
+
+ // Get secondary routes
+ const secondaryRoutes = sourceFile
+ .getVariableDeclaration("secondaryRoutes")
+ ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
+ ?.getElements();
+
+ if (secondaryRoutes) {
+ for (const element of secondaryRoutes) {
+ if (Node.isObjectLiteralExpression(element)) {
+ const labelProp = element
+ .getProperties()
+ .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "label");
+ const hrefProp = element
+ .getProperties()
+ .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href");
+
+ const label = labelProp?.getInitializer()?.getText().replace(/['"]/g, "");
+ const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, "");
+
+ if (label && href) {
+ routes.push({ label, href });
+ }
+ }
+ }
+ }
+
+ return routes;
+};
+
+const removeRouteFromNav = async (project: Project, routeToRemove: string) => {
+ const navFilePath = path.join(state.projectDir, "src/app/navigation.tsx");
+
+ // Skip if there is no navigation file
+ if (!fs.existsSync(navFilePath)) {
+ return;
+ }
+
+ const sourceFile = project.addSourceFileAtPath(navFilePath);
+
+ // Remove from primary routes
+ const primaryRoutes = sourceFile
+ .getVariableDeclaration("primaryRoutes")
+ ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
+
+ if (primaryRoutes) {
+ const elements = primaryRoutes.getElements();
+ for (let i = elements.length - 1; i >= 0; i--) {
+ const element = elements[i];
+ if (Node.isObjectLiteralExpression(element)) {
+ const hrefProp = element
+ .getProperties()
+ .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href");
+
+ const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, "");
+
+ if (href === routeToRemove) {
+ primaryRoutes.removeElement(i);
+ }
+ }
+ }
+ }
+
+ // Remove from secondary routes
+ const secondaryRoutes = sourceFile
+ .getVariableDeclaration("secondaryRoutes")
+ ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
+
+ if (secondaryRoutes) {
+ const elements = secondaryRoutes.getElements();
+ for (let i = elements.length - 1; i >= 0; i--) {
+ const element = elements[i];
+ if (Node.isObjectLiteralExpression(element)) {
+ const hrefProp = element
+ .getProperties()
+ .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href");
+
+ const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, "");
+
+ if (href === routeToRemove) {
+ secondaryRoutes.removeElement(i);
+ }
+ }
+ }
+ }
+
+ await formatAndSaveSourceFiles(project);
+};
+
+export const runRemovePageAction = async (routeName?: string) => {
+ const _settings = getSettings();
+ const projectDir = state.projectDir;
+ const project = getNewProject(projectDir);
+
+ // Get existing routes
+ const routes = getExistingRoutes(project);
+
+ if (routes.length === 0) {
+ return p.cancel("No pages found in the navigation.");
+ }
+
+ let selectedRouteName = routeName;
+ if (!selectedRouteName) {
+ selectedRouteName = abortIfCancel(
+ await p.select({
+ message: "Select the page to remove",
+ options: routes.map((route) => ({
+ label: `${route.label} (${route.href})`,
+ value: route.href,
+ })),
+ }),
+ );
+ }
+
+ if (!selectedRouteName.startsWith("/")) {
+ selectedRouteName = `/${selectedRouteName}`;
+ }
+
+ const pagePath =
+ state.appType === "browser"
+ ? path.join(projectDir, "src/app/(main)", selectedRouteName)
+ : path.join(projectDir, "src/routes", selectedRouteName);
+
+ const spinner = p.spinner();
+ spinner.start("Removing page");
+
+ try {
+ // Check if directory exists
+ if (!fs.existsSync(pagePath)) {
+ spinner.stop("Page not found!");
+ return p.cancel(`Page at ${selectedRouteName} does not exist`);
+ }
+
+ // Remove from navigation first (if present)
+ await removeRouteFromNav(project, selectedRouteName);
+
+ // Remove the page directory
+ await fs.remove(pagePath);
+
+ spinner.stop("Page removed successfully!");
+ } catch (error) {
+ spinner.stop("Failed to remove page!");
+ console.error("Error removing page:", error);
+ process.exit(1);
+ }
+};
+
+export const makeRemovePageCommand = () => {
+ const removePageCommand = new Command("page")
+ .description("Remove a page from your project")
+ .argument("[route]", "The route of the page to remove")
+ .addOption(ciOption)
+ .addOption(debugOption)
+ .action(async (route: string) => {
+ await runRemovePageAction(route);
+ });
+
+ removePageCommand.hook("preAction", (_thisCommand, actionCommand) => {
+ initProgramState(actionCommand.opts());
+ state.baseCommand = "remove";
+ ensureProofKitProject({ commandName: "remove" });
+ });
+
+ return removePageCommand;
+};
diff --git a/packages/cli-old/src/cli/remove/schema.ts b/packages/cli-old/src/cli/remove/schema.ts
new file mode 100644
index 00000000..4cc40088
--- /dev/null
+++ b/packages/cli-old/src/cli/remove/schema.ts
@@ -0,0 +1,100 @@
+import { Command } from "commander";
+import { z } from "zod/v4";
+import * as p from "~/cli/prompts.js";
+
+import { getExistingSchemas, removeLayout } from "~/generators/fmdapi.js";
+import { state } from "~/state.js";
+import { getSettings, type Settings } from "~/utils/parseSettings.js";
+import { abortIfCancel } from "../utils.js";
+
+export const runRemoveSchemaAction = async (opts?: {
+ projectDir?: string;
+ settings?: Settings;
+ sourceName?: string;
+ schemaName?: string;
+}) => {
+ const settings = opts?.settings ?? getSettings();
+ const projectDir = opts?.projectDir ?? state.projectDir;
+ let sourceName = opts?.sourceName;
+
+ // If there is more than one fm data source, prompt for which one to remove from
+ if (!sourceName && settings.dataSources.filter((s) => s.type === "fm").length > 1) {
+ const dataSourceName = await p.select({
+ message: "Which FileMaker data source do you want to remove a layout from?",
+ options: settings.dataSources.filter((s) => s.type === "fm").map((s) => ({ label: s.name, value: s.name })),
+ });
+ if (p.isCancel(dataSourceName)) {
+ p.cancel();
+ process.exit(0);
+ }
+ sourceName = z.string().parse(dataSourceName);
+ }
+
+ if (!sourceName) {
+ sourceName = "filemaker";
+ }
+
+ const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === sourceName);
+ if (!dataSource) {
+ throw new Error(`FileMaker data source ${sourceName} not found in your ProofKit config`);
+ }
+
+ // Get existing schemas for this data source
+ const existingSchemas = getExistingSchemas({
+ projectDir,
+ dataSourceName: sourceName,
+ });
+
+ if (existingSchemas.length === 0) {
+ p.note(`No layouts found in data source "${sourceName}"`, "Nothing to remove");
+ return;
+ }
+
+ // Show existing schemas and let user pick one to remove
+ const schemaToRemove =
+ opts?.schemaName ??
+ abortIfCancel(
+ await p.select({
+ message: "Select a layout to remove",
+ options: existingSchemas
+ .map((schema) => ({
+ label: `${schema.layout} (${schema.schemaName})`,
+ value: schema.schemaName ?? "",
+ }))
+ .filter((opt) => opt.value !== ""),
+ }),
+ );
+
+ // Confirm removal
+ const confirmRemoval = await p.confirm({
+ message: `Are you sure you want to remove the layout "${schemaToRemove}"?`,
+ initialValue: false,
+ });
+
+ if (p.isCancel(confirmRemoval) || !confirmRemoval) {
+ p.cancel("Operation cancelled");
+ process.exit(0);
+ }
+
+ // Remove the schema
+ await removeLayout({
+ projectDir,
+ dataSourceName: sourceName,
+ schemaName: schemaToRemove,
+ runCodegen: true,
+ });
+
+ p.outro(`Layout "${schemaToRemove}" has been removed from your project`);
+};
+
+export const makeRemoveSchemaCommand = () => {
+ const removeSchemaCommand = new Command("layout")
+ .alias("schema")
+ .description("Remove a layout from your fmschema file")
+ .action(async (opts: { settings: Settings }) => {
+ const settings = opts.settings;
+ await runRemoveSchemaAction({ settings });
+ });
+
+ return removeSchemaCommand;
+};
diff --git a/packages/cli-old/src/cli/tanstack-query.ts b/packages/cli-old/src/cli/tanstack-query.ts
new file mode 100644
index 00000000..fb29fac0
--- /dev/null
+++ b/packages/cli-old/src/cli/tanstack-query.ts
@@ -0,0 +1,19 @@
+import { Command } from "commander";
+import * as p from "~/cli/prompts.js";
+
+import { injectTanstackQuery } from "~/generators/tanstack-query.js";
+
+export const runAddTanstackQueryCommand = async () => {
+ const spinner = p.spinner();
+ spinner.start("Adding Tanstack Query");
+ await injectTanstackQuery();
+ spinner.stop("Tanstack Query added");
+};
+
+export const makeAddTanstackQueryCommand = () => {
+ const addTanstackQueryCommand = new Command("tanstack-query")
+ .description("Add Tanstack Query to your project")
+ .action(runAddTanstackQueryCommand);
+
+ return addTanstackQueryCommand;
+};
diff --git a/packages/cli-old/src/cli/typegen/index.ts b/packages/cli-old/src/cli/typegen/index.ts
new file mode 100644
index 00000000..23a4f61c
--- /dev/null
+++ b/packages/cli-old/src/cli/typegen/index.ts
@@ -0,0 +1,20 @@
+import { Command } from "commander";
+
+import { runCodegenCommand } from "~/generators/fmdapi.js";
+import type { Settings } from "~/utils/parseSettings.js";
+import { ensureProofKitProject } from "../utils.js";
+
+export async function runTypegen(_opts: { settings: Settings }) {
+ await runCodegenCommand();
+}
+
+export const makeTypegenCommand = () => {
+ const typegenCommand = new Command("typegen").description("Generate types for your project").action(runTypegen);
+
+ typegenCommand.hook("preAction", (_thisCommand, actionCommand) => {
+ const settings = ensureProofKitProject({ commandName: "typegen" });
+ actionCommand.setOptionValue("settings", settings);
+ });
+
+ return typegenCommand;
+};
diff --git a/packages/cli-old/src/cli/update/index.ts b/packages/cli-old/src/cli/update/index.ts
new file mode 100644
index 00000000..93eca92b
--- /dev/null
+++ b/packages/cli-old/src/cli/update/index.ts
@@ -0,0 +1,28 @@
+import chalk from "chalk";
+import { Command } from "commander";
+
+import { initProgramState, state } from "~/state.js";
+import { runAllAvailableUpgrades } from "~/upgrades/index.js";
+import { logger } from "~/utils/logger.js";
+import { ensureProofKitProject } from "../utils.js";
+
+export const runUpgrade = async () => {
+ initProgramState({});
+ state.baseCommand = "upgrade";
+ ensureProofKitProject({ commandName: "upgrade" });
+
+ logger.info("\nUpgrading ProofKit components...\n");
+
+ try {
+ await runAllAvailableUpgrades();
+ logger.info(chalk.green("✔ Successfully upgraded components\n"));
+ } catch (error) {
+ logger.error("Failed to upgrade components:", error);
+ process.exit(1);
+ }
+};
+
+export const upgrade = new Command()
+ .name("upgrade")
+ .description("Upgrade ProofKit components in your project")
+ .action(runUpgrade);
diff --git a/packages/cli-old/src/cli/update/makeUpgradeCommand.ts b/packages/cli-old/src/cli/update/makeUpgradeCommand.ts
new file mode 100644
index 00000000..8232b3fe
--- /dev/null
+++ b/packages/cli-old/src/cli/update/makeUpgradeCommand.ts
@@ -0,0 +1,25 @@
+import { Command } from "commander";
+
+import { ciOption } from "~/globalOptions.js";
+import { initProgramState, state } from "~/state.js";
+import { ensureProofKitProject } from "../utils.js";
+import { runUpgrade } from "./index.js";
+
+export const makeUpgradeCommand = () => {
+ const upgradeCommand = new Command("upgrade")
+ .description("Upgrade ProofKit components in your project")
+ .addOption(ciOption)
+ .action(async (args) => {
+ initProgramState(args);
+
+ await runUpgrade();
+ });
+
+ upgradeCommand.hook("preAction", (_thisCommand, _actionCommand) => {
+ initProgramState(_actionCommand.opts());
+ state.baseCommand = "upgrade";
+ ensureProofKitProject({ commandName: "upgrade" });
+ });
+
+ return upgradeCommand;
+};
diff --git a/packages/cli-old/src/cli/utils.ts b/packages/cli-old/src/cli/utils.ts
new file mode 100644
index 00000000..37a6897f
--- /dev/null
+++ b/packages/cli-old/src/cli/utils.ts
@@ -0,0 +1,49 @@
+import path from "node:path";
+import chalk from "chalk";
+import fs from "fs-extra";
+import z, { ZodError } from "zod/v4";
+
+import { cancel, isCancel } from "~/cli/prompts.js";
+import { npmName } from "~/consts.js";
+import { getSettings } from "~/utils/parseSettings.js";
+
+/**
+ * Runs before any add command is run. Checks if the user is in a ProofKit project and if the
+ * proofkit.json file is valid.
+ */
+export const ensureProofKitProject = ({ commandName }: { commandName: string }) => {
+ const settingsExists = fs.existsSync(path.join(process.cwd(), "proofkit.json"));
+ if (!settingsExists) {
+ console.log(
+ chalk.yellow(
+ `The "${commandName}" command requires an existing ProofKit project.
+Please run " ${npmName} init" first, or try this command again when inside a ProofKit project.`,
+ ),
+ );
+ process.exit(1);
+ }
+
+ try {
+ return getSettings();
+ } catch (error) {
+ console.log(chalk.red("Error parsing ProofKit settings file:"));
+ if (error instanceof ZodError) {
+ console.log(z.prettifyError(error));
+ } else {
+ console.log(error);
+ }
+
+ process.exit(1);
+ }
+};
+
+export class UserAbortedError extends Error {}
+export function abortIfCancel(value: symbol | string): string;
+export function abortIfCancel(value: symbol | T): T;
+export function abortIfCancel(value: T | symbol): T {
+ if (isCancel(value)) {
+ cancel();
+ throw new UserAbortedError();
+ }
+ return value;
+}
diff --git a/packages/cli-old/src/consts.ts b/packages/cli-old/src/consts.ts
new file mode 100644
index 00000000..d9c413e0
--- /dev/null
+++ b/packages/cli-old/src/consts.ts
@@ -0,0 +1,35 @@
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { getVersion } from "./utils/getProofKitVersion.js";
+
+// Path is in relation to a single index.js file inside ./dist
+const __filename = fileURLToPath(import.meta.url);
+const distPath = path.dirname(__filename);
+export const PKG_ROOT = path.join(distPath, "../");
+export const cliName = "proofkit";
+export const npmName = "@proofkit/cli";
+export const DOCS_URL = "https://proofkit.dev";
+
+const version = getVersion();
+const versionCharLength = version.length;
+//export const PKG_ROOT = path.dirname(require.main.filename);
+
+export const TITLE_TEXT = `
+ _______ ___ ___ ____ _ _
+|_ __ \\ .' ..]|_ ||_ _| (_) / |_
+ | |__) |_ .--. .--. .--. _| |_ | |_/ / __ \`| |-'
+ | ___/[ \`/'\`\\]/ .'\`\\ \\/ .'\`\\ \\'-| |-' | __'. [ | | |
+ _| |_ | | | \\__. || \\__. | | | _| | \\ \\_ | | | |,
+|_____| [___] '.__.' '.__.' [___] |____||____|[___]\\__/
+${" ".repeat(61 - versionCharLength)}v${version}
+`;
+export const DEFAULT_APP_NAME = "my-proofkit-app";
+export const CREATE_FM_APP = cliName;
+
+// Registry URL is injected at build time via tsdown define
+declare const __REGISTRY_URL__: string;
+// Provide a safe fallback when running from source (not built)
+export const DEFAULT_REGISTRY_URL =
+ // typeof check avoids ReferenceError if not defined at runtime
+ typeof __REGISTRY_URL__ !== "undefined" && __REGISTRY_URL__ ? __REGISTRY_URL__ : "https://proofkit.dev";
diff --git a/packages/cli-old/src/generators/auth.ts b/packages/cli-old/src/generators/auth.ts
new file mode 100644
index 00000000..3ecd4080
--- /dev/null
+++ b/packages/cli-old/src/generators/auth.ts
@@ -0,0 +1,83 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import path from "node:path";
+import { glob } from "glob";
+
+import { installDependencies } from "~/helpers/installDependencies.js";
+import { betterAuthInstaller } from "~/installers/better-auth.js";
+import { clerkInstaller } from "~/installers/clerk.js";
+import { proofkitAuthInstaller } from "~/installers/proofkit-auth.js";
+import { state } from "~/state.js";
+import { getSettings, mergeSettings } from "~/utils/parseSettings.js";
+
+export async function addAuth({
+ options,
+ noInstall = false,
+ projectDir = process.cwd(),
+}: {
+ options:
+ | { type: "clerk" }
+ | {
+ type: "fmaddon";
+ emailProvider?: "plunk" | "resend";
+ apiKey?: string;
+ }
+ | { type: "better-auth" };
+ projectDir?: string;
+ noInstall?: boolean;
+}) {
+ const settings = getSettings();
+ if (settings.ui === "shadcn") {
+ throw new Error("Shadcn projects should add auth using the template registry");
+ }
+ if (settings.auth.type !== "none") {
+ throw new Error("Auth already exists");
+ }
+ if (!settings.dataSources.some((o) => o.type === "fm") && options.type === "fmaddon") {
+ throw new Error("A FileMaker data source is required to use the FM Add-on Auth");
+ }
+ if (!settings.dataSources.some((o) => o.type === "fm") && options.type === "better-auth") {
+ throw new Error("A FileMaker data source is required to use the Better-Auth");
+ }
+
+ if (options.type === "clerk") {
+ await addClerkAuth({ projectDir });
+ } else if (options.type === "fmaddon") {
+ await addFmaddonAuth();
+ }
+
+ // Replace actionClient with authedActionClient in all action files
+ await replaceActionClientWithAuthed();
+
+ if (!noInstall) {
+ await installDependencies({ projectDir });
+ }
+}
+
+async function addClerkAuth({ projectDir = process.cwd() }: { projectDir?: string }) {
+ await clerkInstaller({ projectDir });
+ mergeSettings({ auth: { type: "clerk" } });
+}
+
+async function addFmaddonAuth() {
+ await proofkitAuthInstaller();
+ mergeSettings({ auth: { type: "fmaddon" } });
+}
+
+async function replaceActionClientWithAuthed() {
+ const projectDir = state.projectDir;
+ const actionFiles = await glob("src/app/(main)/**/actions.ts", {
+ cwd: projectDir,
+ });
+
+ for (const file of actionFiles) {
+ const fullPath = path.join(projectDir, file);
+ const content = readFileSync(fullPath, "utf-8");
+ const updatedContent = content.replace(/actionClient/g, "authedActionClient");
+ writeFileSync(fullPath, updatedContent);
+ }
+}
+
+async function _addBetterAuth() {
+ await betterAuthInstaller();
+ mergeSettings({ auth: { type: "better-auth" } });
+}
diff --git a/packages/cli-old/src/generators/fmdapi.ts b/packages/cli-old/src/generators/fmdapi.ts
new file mode 100644
index 00000000..a7c16ea9
--- /dev/null
+++ b/packages/cli-old/src/generators/fmdapi.ts
@@ -0,0 +1,525 @@
+import path from "node:path";
+import { generateTypedClients } from "@proofkit/typegen";
+import type { typegenConfigSingle } from "@proofkit/typegen/config";
+import { config as dotenvConfig } from "dotenv";
+import fs from "fs-extra";
+import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
+import { SyntaxKind } from "ts-morph";
+import type { z } from "zod/v4";
+
+import { state } from "~/state.js";
+import { logger } from "~/utils/logger.js";
+import type { envNamesSchema } from "~/utils/parseSettings.js";
+import { getNewProject } from "~/utils/ts-morph.js";
+
+// Input schema for functions like addLayout
+// This might be different from the layout config stored in the file
+interface Schema {
+ layoutName: string;
+ schemaName: string;
+ valueLists?: "strict" | "allowEmpty" | "ignore";
+ generateClient?: boolean;
+ strictNumbers?: boolean;
+}
+
+// For any data source configuration object (fmdapi or fmodata)
+type AnyDataSourceConfig = z.infer;
+// For a single fmdapi data source configuration object
+type FmdapiDataSourceConfig = Extract;
+// For a single layout configuration object within a data source
+type ImportedLayoutConfig = FmdapiDataSourceConfig["layouts"][number];
+
+// This type represents the actual structure of the JSONC file, including $schema
+interface FullProofkitTypegenJsonFile {
+ $schema?: string;
+ config: AnyDataSourceConfig | AnyDataSourceConfig[];
+}
+
+const typegenConfigFileName = "proofkit-typegen.config.jsonc";
+
+// Helper function to normalize data sources by adding default type for backwards compatibility
+// This mirrors the zod preprocess in @proofkit/typegen that defaults type to "fmdapi"
+function normalizeDataSource(ds: AnyDataSourceConfig): AnyDataSourceConfig {
+ if (!("type" in ds) || ds.type === undefined) {
+ return { ...(ds as object), type: "fmdapi" } as AnyDataSourceConfig;
+ }
+ return ds;
+}
+
+function normalizeConfig(
+ config: AnyDataSourceConfig | AnyDataSourceConfig[],
+): AnyDataSourceConfig | AnyDataSourceConfig[] {
+ if (Array.isArray(config)) {
+ return config.map(normalizeDataSource);
+ }
+ return normalizeDataSource(config);
+}
+
+// Helper functions for JSON config
+async function readJsonConfigFile(configPath: string): Promise {
+ if (!fs.existsSync(configPath)) {
+ return null;
+ }
+ try {
+ const fileContent = await fs.readFile(configPath, "utf8");
+ const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile;
+ // Normalize config to add default type for backwards compatibility
+ if (parsed.config) {
+ parsed.config = normalizeConfig(parsed.config);
+ }
+ return parsed;
+ } catch (error) {
+ console.error(`Error reading or parsing JSONC config at ${configPath}:`, error);
+ // Return a default structure for the *file* if parsing fails but file exists
+ return {
+ $schema: "https://proofkit.dev/typegen-config-schema.json",
+ config: [],
+ };
+ }
+}
+
+async function writeJsonConfigFile(configPath: string, fileContent: FullProofkitTypegenJsonFile) {
+ // Check if file exists to preserve comments
+ if (fs.existsSync(configPath)) {
+ const originalText = await fs.readFile(configPath, "utf8");
+ // Use jsonc-parser's modify function to preserve comments
+ const edits = modify(originalText, ["config"], fileContent.config, {
+ formattingOptions: {
+ tabSize: 2,
+ insertSpaces: true,
+ eol: "\n",
+ },
+ });
+ const modifiedText = applyEdits(originalText, edits);
+ await fs.writeFile(configPath, modifiedText, "utf8");
+ } else {
+ // If file doesn't exist, create it with proper formatting
+ await fs.writeJson(configPath, fileContent, { spaces: 2 });
+ }
+}
+
+export async function addLayout({
+ projectDir = process.cwd(),
+ schemas,
+ runCodegen = true,
+ dataSourceName,
+}: {
+ projectDir?: string;
+ schemas: Schema[];
+ runCodegen?: boolean;
+ dataSourceName: string;
+}) {
+ const jsonConfigPath = path.join(projectDir, typegenConfigFileName);
+ let fileContent = await readJsonConfigFile(jsonConfigPath);
+
+ if (!fileContent) {
+ fileContent = {
+ $schema: "https://proofkit.dev/typegen-config-schema.json",
+ config: [],
+ };
+ }
+
+ // Work with the 'config' property which is TypegenConfig['config']
+ const configProperty = fileContent.config;
+
+ let configArray: AnyDataSourceConfig[];
+ if (Array.isArray(configProperty)) {
+ configArray = configProperty;
+ } else {
+ configArray = [configProperty];
+ fileContent.config = configArray; // Update fileContent to ensure it's an array for later ops
+ }
+
+ const layoutsToAdd: ImportedLayoutConfig[] = schemas.map((schema) => ({
+ layoutName: schema.layoutName,
+ schemaName: schema.schemaName,
+ valueLists: schema.valueLists,
+ generateClient: schema.generateClient,
+ strictNumbers: schema.strictNumbers,
+ }));
+
+ let targetDataSource: FmdapiDataSourceConfig | undefined = configArray.find(
+ (ds): ds is FmdapiDataSourceConfig =>
+ ds.type === "fmdapi" &&
+ (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName),
+ );
+
+ if (targetDataSource) {
+ targetDataSource.layouts = targetDataSource.layouts || [];
+ } else {
+ targetDataSource = {
+ type: "fmdapi",
+ layouts: [],
+ path: `./src/config/schemas/${dataSourceName}`,
+ // other default properties for a new DataSourceConfig can be added here if needed
+ envNames: undefined,
+ };
+ configArray.push(targetDataSource);
+ }
+
+ targetDataSource.layouts.push(...layoutsToAdd);
+ // fileContent.config is already pointing to configArray if it was modified
+
+ await writeJsonConfigFile(jsonConfigPath, fileContent);
+
+ if (runCodegen) {
+ await runCodegenCommand();
+ }
+}
+
+export async function addConfig({
+ config,
+ projectDir,
+ runCodegen = true,
+}: {
+ config: FmdapiDataSourceConfig | FmdapiDataSourceConfig[];
+ projectDir: string;
+ runCodegen?: boolean;
+}) {
+ const jsonConfigPath = path.join(projectDir, typegenConfigFileName);
+ let fileContent = await readJsonConfigFile(jsonConfigPath);
+
+ const configsToAdd = Array.isArray(config) ? config : [config];
+
+ if (fileContent) {
+ if (Array.isArray(fileContent.config)) {
+ fileContent.config.push(...configsToAdd);
+ } else {
+ fileContent.config = [fileContent.config, ...configsToAdd];
+ }
+ } else {
+ fileContent = {
+ $schema: "https://proofkit.dev/typegen-config-schema.json",
+ config: configsToAdd,
+ };
+ }
+
+ await writeJsonConfigFile(jsonConfigPath, fileContent);
+
+ if (runCodegen) {
+ await runCodegenCommand();
+ }
+}
+
+export async function ensureWebviewerFmHttpConfig({
+ projectDir,
+ connectedFileName,
+ dataSourceName = "filemaker",
+ baseUrl,
+}: {
+ projectDir: string;
+ connectedFileName?: string;
+ dataSourceName?: string;
+ baseUrl?: string;
+}) {
+ const newConfig: FmdapiDataSourceConfig = {
+ type: "fmdapi",
+ path: `./src/config/schemas/${dataSourceName}`,
+ clearOldFiles: true,
+ clientSuffix: "Layout",
+ webviewerScriptName: "ExecuteDataApi",
+ envNames: undefined,
+ layouts: [],
+ fmHttp: {
+ enabled: true,
+ ...(baseUrl ? { baseUrl } : {}),
+ ...(connectedFileName ? { connectedFileName } : {}),
+ },
+ };
+
+ const jsonConfigPath = path.join(projectDir, typegenConfigFileName);
+ let fileContent = await readJsonConfigFile(jsonConfigPath);
+
+ if (!fileContent) {
+ fileContent = {
+ $schema: "https://proofkit.dev/typegen-config-schema.json",
+ config: [newConfig],
+ };
+ await writeJsonConfigFile(jsonConfigPath, fileContent);
+ return;
+ }
+
+ const configArray = Array.isArray(fileContent.config) ? fileContent.config : [fileContent.config];
+ if (!Array.isArray(fileContent.config)) {
+ fileContent.config = configArray;
+ }
+
+ const existingConfigIndex = configArray.findIndex(
+ (config): config is FmdapiDataSourceConfig => config.type === "fmdapi" && config.path === newConfig.path,
+ );
+
+ if (existingConfigIndex === -1) {
+ configArray.push(newConfig);
+ } else {
+ const existingConfig = configArray[existingConfigIndex] as FmdapiDataSourceConfig;
+ configArray[existingConfigIndex] = {
+ ...existingConfig,
+ ...newConfig,
+ layouts: existingConfig.layouts ?? [],
+ fmHttp: {
+ enabled: true,
+ ...(existingConfig.fmHttp ?? {}),
+ ...(newConfig.fmHttp ?? {}),
+ },
+ };
+ }
+
+ await writeJsonConfigFile(jsonConfigPath, fileContent);
+}
+
+export async function runCodegenCommand() {
+ const projectDir = state.projectDir;
+ const config = await readJsonConfigFile(path.join(projectDir, typegenConfigFileName));
+ if (!config) {
+ logger.info("no typegen config found, skipping typegen");
+ return;
+ }
+
+ // make sure to load the .env file
+ dotenvConfig({ path: path.join(projectDir, ".env") });
+ await generateTypedClients(config.config, { cwd: projectDir });
+}
+
+export function getClientSuffix({
+ projectDir = process.cwd(),
+ dataSourceName,
+}: {
+ projectDir?: string;
+ dataSourceName: string;
+}): string {
+ const jsonConfigPath = path.join(projectDir, typegenConfigFileName);
+ if (!fs.existsSync(jsonConfigPath)) {
+ return "Client";
+ }
+ try {
+ const fileContent = fs.readFileSync(jsonConfigPath, "utf8");
+ const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile;
+
+ // Normalize config to add default type for backwards compatibility
+ const normalizedConfig = normalizeConfig(parsed.config);
+ const configToSearch = Array.isArray(normalizedConfig) ? normalizedConfig : [normalizedConfig];
+
+ const targetDataSource = configToSearch.find(
+ (ds): ds is FmdapiDataSourceConfig =>
+ ds.type === "fmdapi" &&
+ (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName),
+ );
+ return targetDataSource?.clientSuffix ?? "Client";
+ } catch (error) {
+ console.error(`Error reading or parsing JSONC config for getClientSuffix: ${jsonConfigPath}`, error);
+ return "Client";
+ }
+}
+
+export function getExistingSchemas({
+ projectDir = process.cwd(),
+ dataSourceName,
+}: {
+ projectDir?: string;
+ dataSourceName: string;
+}): { layout?: string; schemaName?: string }[] {
+ const jsonConfigPath = path.join(projectDir, typegenConfigFileName);
+ if (!fs.existsSync(jsonConfigPath)) {
+ return [];
+ }
+ try {
+ const fileContent = fs.readFileSync(jsonConfigPath, "utf8");
+ const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile;
+
+ // Normalize config to add default type for backwards compatibility
+ const normalizedConfig = normalizeConfig(parsed.config);
+ const configToSearch = Array.isArray(normalizedConfig) ? normalizedConfig : [normalizedConfig];
+
+ const targetDataSource = configToSearch.find(
+ (ds): ds is FmdapiDataSourceConfig =>
+ ds.type === "fmdapi" &&
+ (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName),
+ );
+
+ if (targetDataSource?.layouts) {
+ return targetDataSource.layouts.map((layout) => ({
+ layout: layout.layoutName,
+ schemaName: layout.schemaName,
+ }));
+ }
+ return [];
+ } catch (error) {
+ console.error(`Error reading or parsing JSONC config for getExistingSchemas: ${jsonConfigPath}`, error);
+ return [];
+ }
+}
+
+export async function addToFmschemaConfig({
+ dataSourceName,
+ envNames,
+}: {
+ dataSourceName: string;
+ envNames?: z.infer;
+}) {
+ const projectDir = state.projectDir;
+ const jsonConfigPath = path.join(projectDir, typegenConfigFileName);
+ let fileContent = await readJsonConfigFile(jsonConfigPath);
+
+ const newDataSource: FmdapiDataSourceConfig = {
+ type: "fmdapi",
+ layouts: [],
+ path: `./src/config/schemas/${dataSourceName}`,
+ envNames: undefined,
+ clearOldFiles: true,
+ clientSuffix: "Layout",
+ };
+
+ if (envNames) {
+ newDataSource.envNames = {
+ server: envNames.server,
+ db: envNames.database,
+ auth: { apiKey: envNames.apiKey },
+ };
+ }
+ if (state.appType === "webviewer") {
+ newDataSource.webviewerScriptName = "ExecuteDataApi";
+ }
+
+ if (fileContent) {
+ let configArray: AnyDataSourceConfig[];
+ if (Array.isArray(fileContent.config)) {
+ configArray = fileContent.config;
+ } else {
+ configArray = [fileContent.config];
+ fileContent.config = configArray;
+ }
+
+ const existingDsIndex = configArray.findIndex((ds) => ds.type === "fmdapi" && ds.path === newDataSource.path);
+ if (existingDsIndex === -1) {
+ configArray.push(newDataSource);
+ } else {
+ const existingConfig = configArray[existingDsIndex] as FmdapiDataSourceConfig;
+ configArray[existingDsIndex] = {
+ ...existingConfig,
+ ...newDataSource,
+ layouts: newDataSource.layouts.length > 0 ? newDataSource.layouts : existingConfig.layouts || [],
+ };
+ }
+ } else {
+ fileContent = {
+ $schema: "https://proofkit.dev/typegen-config-schema.json",
+ config: [newDataSource],
+ };
+ }
+ await writeJsonConfigFile(jsonConfigPath, fileContent);
+}
+
+export function getFieldNamesForSchema({ schemaName, dataSourceName }: { schemaName: string; dataSourceName: string }) {
+ const projectDir = state.projectDir;
+ const project = getNewProject(projectDir);
+ const sourceFilePath = path.join(projectDir, `src/config/schemas/${dataSourceName}/generated/${schemaName}.ts`);
+
+ const sourceFilePathAlternative = path.join(projectDir, `src/config/schemas/${dataSourceName}/${schemaName}.ts`);
+
+ let fileToUse = sourceFilePath;
+ if (!fs.existsSync(sourceFilePath)) {
+ if (fs.existsSync(sourceFilePathAlternative)) {
+ fileToUse = sourceFilePathAlternative;
+ } else {
+ return [];
+ }
+ }
+ const sourceFile = project.addSourceFileAtPath(fileToUse);
+
+ const zodSchema = sourceFile.getVariableDeclaration(`Z${schemaName}`);
+ if (zodSchema) {
+ const properties = zodSchema
+ .getInitializer()
+ ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression)
+ ?.getProperties();
+ return (
+ properties?.map((pr) => pr.asKind(SyntaxKind.PropertyAssignment)?.getName()?.replace(/"/g, "")).filter(Boolean) ??
+ []
+ );
+ }
+ const typeAlias = sourceFile.getTypeAlias(`T${schemaName}`);
+ const properties = typeAlias?.getFirstDescendantByKind(SyntaxKind.TypeLiteral)?.getProperties();
+ return (
+ properties?.map((pr) => pr.asKind(SyntaxKind.PropertySignature)?.getName()?.replace(/"/g, "")).filter(Boolean) ?? []
+ );
+}
+
+export async function removeFromFmschemaConfig({ dataSourceName }: { dataSourceName: string }) {
+ const projectDir = state.projectDir;
+ const jsonConfigPath = path.join(projectDir, typegenConfigFileName);
+ const fileContent = await readJsonConfigFile(jsonConfigPath);
+
+ if (!fileContent) {
+ return;
+ }
+
+ const pathToRemove = `./src/config/schemas/${dataSourceName}`;
+
+ if (Array.isArray(fileContent.config)) {
+ fileContent.config = fileContent.config.filter((ds) => !(ds.type === "fmdapi" && ds.path === pathToRemove));
+ } else {
+ const currentConfig = fileContent.config;
+ if (currentConfig.type === "fmdapi" && currentConfig.path === pathToRemove) {
+ fileContent.config = [];
+ }
+ }
+ await writeJsonConfigFile(jsonConfigPath, fileContent);
+}
+
+export async function removeLayout({
+ projectDir = state.projectDir,
+ schemaName,
+ dataSourceName,
+ runCodegen = true,
+}: {
+ projectDir?: string;
+ schemaName: string;
+ dataSourceName: string;
+ runCodegen?: boolean;
+}) {
+ const jsonConfigPath = path.join(projectDir, typegenConfigFileName);
+ const fileContent = await readJsonConfigFile(jsonConfigPath);
+
+ if (!fileContent) {
+ throw new Error(`${typegenConfigFileName} not found, cannot remove layout.`);
+ }
+
+ let dataSourceModified = false;
+ const targetDsPath = `./src/config/schemas/${dataSourceName}`;
+
+ let configArray: AnyDataSourceConfig[];
+ if (Array.isArray(fileContent.config)) {
+ configArray = fileContent.config;
+ } else {
+ configArray = [fileContent.config];
+ fileContent.config = configArray;
+ }
+
+ const targetDataSource = configArray.find(
+ (ds): ds is FmdapiDataSourceConfig => ds.type === "fmdapi" && ds.path === targetDsPath,
+ );
+
+ if (targetDataSource?.layouts) {
+ const initialCount = targetDataSource.layouts.length;
+ targetDataSource.layouts = targetDataSource.layouts.filter((layout) => layout.schemaName !== schemaName);
+ if (targetDataSource.layouts.length < initialCount) {
+ dataSourceModified = true;
+ }
+ }
+
+ if (dataSourceModified) {
+ await writeJsonConfigFile(jsonConfigPath, fileContent);
+ }
+
+ const schemaFilePath = path.join(projectDir, "src", "config", "schemas", dataSourceName, `${schemaName}.ts`);
+ if (fs.existsSync(schemaFilePath)) {
+ fs.removeSync(schemaFilePath);
+ }
+
+ if (runCodegen && dataSourceModified) {
+ await runCodegenCommand();
+ }
+}
+
+// Make sure to remove unused imports like Project, SyntaxKind, etc. if they are no longer used anywhere.
+// Also remove getNewProject and formatAndSaveSourceFiles from imports if they were only for config.
diff --git a/packages/cli-old/src/generators/route.ts b/packages/cli-old/src/generators/route.ts
new file mode 100644
index 00000000..512c9a49
--- /dev/null
+++ b/packages/cli-old/src/generators/route.ts
@@ -0,0 +1,40 @@
+import path from "node:path";
+import fs from "fs-extra";
+import type { RouteLink } from "index.js";
+import { SyntaxKind } from "ts-morph";
+
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+
+export async function addRouteToNav({
+ projectDir,
+ navType,
+ ...route
+}: Omit & {
+ projectDir: string;
+ navType: "primary" | "secondary";
+}) {
+ const navFilePath = path.join(projectDir, "src/app/navigation.tsx");
+
+ // If the navigation file doesn't exist (e.g., WebViewer apps), skip adding to nav
+ if (!fs.existsSync(navFilePath)) {
+ return;
+ }
+
+ const project = getNewProject(projectDir);
+ const sourceFile = project.addSourceFileAtPath(navFilePath);
+ sourceFile
+ .getVariableDeclaration(navType === "primary" ? "primaryRoutes" : "secondaryRoutes")
+ ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
+ ?.addElement((writer) =>
+ writer
+ .block(() => {
+ writer.write(`
+ label: "${route.label}",
+ type: "link",
+ href: "${route.href}",`);
+ })
+ .write(","),
+ );
+
+ await formatAndSaveSourceFiles(project);
+}
diff --git a/packages/cli-old/src/generators/tanstack-query.ts b/packages/cli-old/src/generators/tanstack-query.ts
new file mode 100644
index 00000000..874eee0d
--- /dev/null
+++ b/packages/cli-old/src/generators/tanstack-query.ts
@@ -0,0 +1,97 @@
+import path from "node:path";
+import fs from "fs-extra";
+import { type Project, SyntaxKind } from "ts-morph";
+
+import { PKG_ROOT } from "~/consts.js";
+import { state } from "~/state.js";
+import { addPackageDependency } from "~/utils/addPackageDependency.js";
+import { getSettings, setSettings } from "~/utils/parseSettings.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+
+export async function injectTanstackQuery(args?: { project?: Project }) {
+ const projectDir = state.projectDir;
+ const settings = getSettings();
+ if (settings.ui === "shadcn") {
+ return false;
+ }
+ if (settings.tanstackQuery) {
+ return false;
+ }
+
+ addPackageDependency({
+ projectDir,
+ dependencies: ["@tanstack/react-query"],
+ devMode: false,
+ });
+ addPackageDependency({
+ projectDir,
+ dependencies: ["@tanstack/react-query-devtools"],
+ devMode: true,
+ });
+ const extrasDir = path.join(PKG_ROOT, "template", "extras");
+
+ if (state.appType === "browser") {
+ fs.copySync(
+ path.join(extrasDir, "config", "get-query-client.ts"),
+ path.join(projectDir, "src/config/get-query-client.ts"),
+ );
+ fs.copySync(
+ path.join(extrasDir, "config", "query-provider.tsx"),
+ path.join(projectDir, "src/config/query-provider.tsx"),
+ );
+ } else if (state.appType === "webviewer") {
+ fs.copySync(
+ path.join(extrasDir, "config", "query-provider-vite.tsx"),
+ path.join(projectDir, "src/config/query-provider.tsx"),
+ );
+ }
+
+ // inject query provider into the root layout
+ const project = args?.project ?? getNewProject(projectDir);
+ const rootLayout = project.addSourceFileAtPath(
+ path.join(projectDir, state.appType === "browser" ? "src/app/layout.tsx" : "src/main.tsx"),
+ );
+ rootLayout.addImportDeclaration({
+ moduleSpecifier: "@/config/query-provider",
+ defaultImport: "QueryProvider",
+ });
+
+ if (state.appType === "browser") {
+ const exportDefault = rootLayout.getFunction((dec) => dec.isDefaultExport());
+ const bodyElement = exportDefault
+ ?.getBody()
+ ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement)
+ ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
+ .find((openingElement) => openingElement.getTagNameNode().getText() === "body")
+ ?.getParentIfKind(SyntaxKind.JsxElement);
+
+ const childrenText = bodyElement
+ ?.getJsxChildren()
+ .map((child) => child.getText())
+ .filter(Boolean)
+ .join("\n");
+
+ bodyElement?.getChildSyntaxList()?.replaceWithText(
+ `
+ ${childrenText}
+ `,
+ );
+ } else if (state.appType === "webviewer") {
+ const mantineProvider = rootLayout
+ .getDescendantsOfKind(SyntaxKind.JsxElement)
+ .find((element) => element.getOpeningElement().getTagNameNode().getText() === "MantineProvider");
+
+ mantineProvider?.replaceWithText(
+ `
+ ${mantineProvider.getText()}
+ `,
+ );
+ }
+
+ if (!args?.project) {
+ await formatAndSaveSourceFiles(project);
+ }
+
+ setSettings({ ...settings, tanstackQuery: true });
+ return true;
+}
diff --git a/packages/cli-old/src/globalOptions.ts b/packages/cli-old/src/globalOptions.ts
new file mode 100644
index 00000000..5fbdeef7
--- /dev/null
+++ b/packages/cli-old/src/globalOptions.ts
@@ -0,0 +1,8 @@
+import { Option } from "commander";
+
+export const ciOption = new Option("--ci", "Deprecated alias for --non-interactive").default(false);
+export const nonInteractiveOption = new Option(
+ "--non-interactive",
+ "Never prompt for input; fail with a clear error when required values are missing",
+).default(false);
+export const debugOption = new Option("--debug", "Run in debug mode").default(false);
diff --git a/packages/cli-old/src/globals.d.ts b/packages/cli-old/src/globals.d.ts
new file mode 100644
index 00000000..edd6438c
--- /dev/null
+++ b/packages/cli-old/src/globals.d.ts
@@ -0,0 +1,4 @@
+declare const __FMDAPI_VERSION__: string;
+declare const __BETTER_AUTH_VERSION__: string;
+declare const __WEBVIEWER_VERSION__: string;
+declare const __TYPEGEN_VERSION__: string;
diff --git a/packages/cli-old/src/helpers/createProject.ts b/packages/cli-old/src/helpers/createProject.ts
new file mode 100644
index 00000000..cb354d95
--- /dev/null
+++ b/packages/cli-old/src/helpers/createProject.ts
@@ -0,0 +1,129 @@
+import path from "node:path";
+
+import { installPackages } from "~/helpers/installPackages.js";
+import { scaffoldProject } from "~/helpers/scaffoldProject.js";
+import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js";
+import type { PkgInstallerMap } from "~/installers/index.js";
+import { state } from "~/state.js";
+import { addPackageDependency } from "~/utils/addPackageDependency.js";
+import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
+import { replaceTextInFiles } from "./replaceText.js";
+
+interface CreateProjectOptions {
+ projectName: string;
+ packages: PkgInstallerMap;
+ scopedAppName: string;
+ noInstall: boolean;
+ force: boolean;
+ appRouter: boolean;
+}
+
+export const createBareProject = async ({
+ projectName,
+ scopedAppName,
+ packages,
+ noInstall,
+ force,
+}: CreateProjectOptions) => {
+ const pkgManager = getUserPkgManager();
+ state.projectDir = path.resolve(process.cwd(), projectName);
+
+ // Bootstraps the base Next.js application
+ await scaffoldProject({
+ projectName,
+ pkgManager,
+ scopedAppName,
+ noInstall,
+ force,
+ });
+
+ addPackageDependency({
+ dependencies: ["@proofkit/cli", "@types/node"],
+ devMode: true,
+ });
+
+ // Add new base dependencies for Tailwind v4 and shadcn/ui or legacy Mantine
+ // These should match the plan and dependencyVersionMap
+ const NEXT_SHADCN_BASE_DEPS = [
+ "@radix-ui/react-slot",
+ "@tailwindcss/postcss",
+ "class-variance-authority",
+ "clsx",
+ "lucide-react",
+ "tailwind-merge",
+ "tailwindcss",
+ "tw-animate-css",
+ "next-themes",
+ ] as AvailableDependencies[];
+ const VITE_SHADCN_BASE_DEPS = [
+ "@radix-ui/react-slot",
+ "@tailwindcss/vite",
+ "@proofkit/fmdapi",
+ "@proofkit/webviewer",
+ "class-variance-authority",
+ "clsx",
+ "lucide-react",
+ "tailwind-merge",
+ "tailwindcss",
+ "tw-animate-css",
+ "zod",
+ ] as AvailableDependencies[];
+ const SHADCN_BASE_DEV_DEPS = [] as AvailableDependencies[];
+ const VITE_SHADCN_BASE_DEV_DEPS = ["@proofkit/typegen"] as AvailableDependencies[];
+
+ const MANTINE_DEPS = [
+ "@mantine/core",
+ "@mantine/dates",
+ "@mantine/hooks",
+ "@mantine/modals",
+ "@mantine/notifications",
+ "mantine-react-table",
+ ] as AvailableDependencies[];
+ const MANTINE_DEV_DEPS = ["postcss", "postcss-preset-mantine", "postcss-simple-vars"] as AvailableDependencies[];
+
+ if (state.ui === "mantine") {
+ addPackageDependency({
+ dependencies: MANTINE_DEPS,
+ devMode: false,
+ });
+ addPackageDependency({
+ dependencies: MANTINE_DEV_DEPS,
+ devMode: true,
+ });
+ } else if (state.ui === "shadcn") {
+ addPackageDependency({
+ dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEPS : NEXT_SHADCN_BASE_DEPS,
+ devMode: false,
+ });
+ addPackageDependency({
+ dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEV_DEPS : SHADCN_BASE_DEV_DEPS,
+ devMode: true,
+ });
+ } else {
+ throw new Error(`Unsupported UI library: ${state.ui}`);
+ }
+
+ // Install the selected packages
+ installPackages({
+ projectName,
+ scopedAppName,
+ pkgManager,
+ packages,
+ noInstall,
+ });
+
+ let pkgManagerCommand: string;
+ if (pkgManager === "pnpm") {
+ pkgManagerCommand = "pnpm";
+ } else if (pkgManager === "bun") {
+ pkgManagerCommand = "bun";
+ } else if (pkgManager === "yarn") {
+ pkgManagerCommand = "yarn";
+ } else {
+ pkgManagerCommand = "npm run";
+ }
+
+ replaceTextInFiles(state.projectDir, "__PNPM_COMMAND__", pkgManagerCommand);
+
+ return state.projectDir;
+};
diff --git a/packages/cli-old/src/helpers/fmHttp.ts b/packages/cli-old/src/helpers/fmHttp.ts
new file mode 100644
index 00000000..89799b75
--- /dev/null
+++ b/packages/cli-old/src/helpers/fmHttp.ts
@@ -0,0 +1,56 @@
+const defaultBaseUrl = process.env.FM_HTTP_BASE_URL ?? "http://127.0.0.1:1365";
+const REQUEST_TIMEOUT_MS = 3000;
+
+export interface FmHttpStatus {
+ baseUrl: string;
+ healthy: boolean;
+ connectedFiles: string[];
+}
+
+async function fetchWithTimeout(url: string): Promise {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
+
+ try {
+ return await fetch(url, { signal: controller.signal });
+ } catch {
+ return null;
+ } finally {
+ clearTimeout(timeoutId);
+ }
+}
+
+async function readJson(url: string): Promise {
+ const response = await fetchWithTimeout(url);
+
+ if (!response?.ok) {
+ return null;
+ }
+
+ return await response.json().catch(() => null);
+}
+
+export async function getFmHttpStatus(baseUrl = defaultBaseUrl): Promise {
+ const healthResponse = await fetchWithTimeout(`${baseUrl}/health`);
+
+ if (!healthResponse?.ok) {
+ return {
+ baseUrl,
+ healthy: false,
+ connectedFiles: [],
+ };
+ }
+
+ const connectedFiles = await readJson(`${baseUrl}/connectedFiles`);
+
+ return {
+ baseUrl,
+ healthy: true,
+ connectedFiles: Array.isArray(connectedFiles) ? connectedFiles : [],
+ };
+}
+
+export async function detectConnectedFmFile(baseUrl = defaultBaseUrl): Promise {
+ const status = await getFmHttpStatus(baseUrl);
+ return status.connectedFiles[0];
+}
diff --git a/packages/cli-old/src/helpers/git.ts b/packages/cli-old/src/helpers/git.ts
new file mode 100644
index 00000000..bdeaefee
--- /dev/null
+++ b/packages/cli-old/src/helpers/git.ts
@@ -0,0 +1,140 @@
+import { execSync } from "node:child_process";
+import path from "node:path";
+import chalk from "chalk";
+import { execa } from "execa";
+import fs from "fs-extra";
+import ora from "ora";
+import * as p from "~/cli/prompts.js";
+
+import { isNonInteractiveMode } from "~/state.js";
+import { logger } from "~/utils/logger.js";
+
+const isGitInstalled = (dir: string): boolean => {
+ try {
+ execSync("git --version", { cwd: dir });
+ return true;
+ } catch (_e) {
+ return false;
+ }
+};
+
+/** @returns Whether or not the provided directory has a `.git` subdirectory in it. */
+export const isRootGitRepo = (dir: string): boolean => {
+ return fs.existsSync(path.join(dir, ".git"));
+};
+
+/** @returns Whether or not this directory or a parent directory has a `.git` directory. */
+export const isInsideGitRepo = async (dir: string): Promise => {
+ try {
+ // If this command succeeds, we're inside a git repo
+ await execa("git", ["rev-parse", "--is-inside-work-tree"], {
+ cwd: dir,
+ stdout: "ignore",
+ });
+ return true;
+ } catch (_e) {
+ // Else, it will throw a git-error and we return false
+ return false;
+ }
+};
+
+const getGitVersion = () => {
+ const stdout = execSync("git --version").toString().trim();
+ const gitVersionTag = stdout.split(" ")[2];
+ const major = gitVersionTag?.split(".")[0];
+ const minor = gitVersionTag?.split(".")[1];
+ return { major: Number(major), minor: Number(minor) };
+};
+
+/** @returns The git config value of "init.defaultBranch". If it is not set, returns "main". */
+const getDefaultBranch = () => {
+ const stdout = execSync("git config --global init.defaultBranch || echo main").toString().trim();
+
+ return stdout;
+};
+
+// This initializes the Git-repository for the project
+export const initializeGit = async (projectDir: string) => {
+ logger.info("Initializing Git...");
+
+ if (!isGitInstalled(projectDir)) {
+ logger.warn("Git is not installed. Skipping Git initialization.");
+ return;
+ }
+
+ const spinner = ora("Creating a new git repo...\n").start();
+
+ const isRoot = isRootGitRepo(projectDir);
+ const isInside = await isInsideGitRepo(projectDir);
+ const dirName = path.parse(projectDir).name; // skip full path for logging
+
+ if (isInside && isRoot) {
+ // Dir is a root git repo
+ spinner.stop();
+ if (isNonInteractiveMode()) {
+ throw new Error(
+ `Cannot initialize git in non-interactive mode because "${dirName}" already contains a git repository.`,
+ );
+ }
+ const overwriteGit = await p.confirm({
+ message: `${chalk.redBright.bold(
+ "Warning:",
+ )} Git is already initialized in "${dirName}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,
+ initialValue: false,
+ });
+
+ if (!overwriteGit) {
+ spinner.info("Skipping Git initialization.");
+ return;
+ }
+ // Deleting the .git folder
+ fs.removeSync(path.join(projectDir, ".git"));
+ } else if (isInside && !isRoot) {
+ // Dir is inside a git worktree
+ spinner.stop();
+ if (isNonInteractiveMode()) {
+ throw new Error(
+ `Cannot initialize git in non-interactive mode because "${dirName}" is already inside a git worktree.`,
+ );
+ }
+ const initializeChildGitRepo = await p.confirm({
+ message: `${chalk.redBright.bold(
+ "Warning:",
+ )} "${dirName}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`,
+ initialValue: false,
+ });
+ if (!initializeChildGitRepo) {
+ spinner.info("Skipping Git initialization.");
+ return;
+ }
+ }
+
+ // We're good to go, initializing the git repo
+ try {
+ const branchName = getDefaultBranch();
+
+ // --initial-branch flag was added in git v2.28.0
+ const { major, minor } = getGitVersion();
+ if (major < 2 || (major === 2 && minor < 28)) {
+ await execa("git", ["init"], { cwd: projectDir });
+ // symbolic-ref is used here due to refs/heads/master not existing
+ // It is only created after the first commit
+ // https://superuser.com/a/1419674
+ await execa("git", ["symbolic-ref", "HEAD", `refs/heads/${branchName}`], {
+ cwd: projectDir,
+ });
+ } else {
+ await execa("git", ["init", `--initial-branch=${branchName}`], {
+ cwd: projectDir,
+ });
+ }
+ await execa("git", ["add", "."], { cwd: projectDir });
+ await execa("git", ["commit", "-m", "Initial commit"], {
+ cwd: projectDir,
+ });
+ spinner.succeed(`${chalk.green("Successfully initialized and staged")} ${chalk.green.bold("git")}\n`);
+ } catch (_error) {
+ // Safeguard, should be unreachable
+ spinner.fail(`${chalk.bold.red("Failed:")} could not initialize git. Update git to the latest version!\n`);
+ }
+};
diff --git a/packages/cli-old/src/helpers/installDependencies.ts b/packages/cli-old/src/helpers/installDependencies.ts
new file mode 100644
index 00000000..880bd436
--- /dev/null
+++ b/packages/cli-old/src/helpers/installDependencies.ts
@@ -0,0 +1,242 @@
+import chalk from "chalk";
+import { execa, type StdoutStderrOption } from "execa";
+import ora, { type Ora } from "ora";
+
+import { state } from "~/state.js";
+import { getUserPkgManager, type PackageManager } from "~/utils/getUserPkgManager.js";
+import { logger } from "~/utils/logger.js";
+
+const execWithSpinner = async (
+ projectDir: string,
+ pkgManager: PackageManager | "pnpx" | "bunx",
+ options: {
+ args?: string[];
+ stdout?: StdoutStderrOption;
+ onDataHandle?: (spinner: Ora) => (data: Buffer) => void;
+ loadingMessage?: string;
+ },
+) => {
+ const { onDataHandle, args = ["install"], stdout = "pipe" } = options;
+
+ if (process.env.PROOFKIT_ENV === "development") {
+ args.push("--prefer-offline");
+ }
+
+ const spinner = ora(options.loadingMessage ?? `Running ${pkgManager} ${args.join(" ")} ...`).start();
+ const subprocess = execa(pkgManager, args, {
+ cwd: projectDir,
+ stdout,
+ stderr: "pipe", // Capture stderr to get error messages
+ });
+
+ await new Promise((res, rej) => {
+ let stdoutOutput = "";
+ let stderrOutput = "";
+
+ if (onDataHandle) {
+ subprocess.stdout?.on("data", onDataHandle(spinner));
+ } else {
+ // If no custom handler, capture stdout for error reporting
+ subprocess.stdout?.on("data", (data) => {
+ stdoutOutput += data.toString();
+ });
+ }
+
+ // Capture stderr output for error reporting
+ subprocess.stderr?.on("data", (data) => {
+ stderrOutput += data.toString();
+ });
+
+ subprocess.on("error", (e) => rej(e));
+ subprocess.on("close", (code) => {
+ if (code === 0) {
+ res();
+ } else {
+ // Combine stdout and stderr for complete error message
+ const combinedOutput = [stdoutOutput, stderrOutput]
+ .filter((output) => output.trim())
+ .join("\n")
+ .trim()
+ // Remove spinner-related lines that aren't useful in error output
+ .replace(/^- Checking registry\.$/gm, "")
+ .replace(/^\s*$/gm, "") // Remove empty lines
+ .trim();
+
+ const errorMessage = combinedOutput || `Command failed with exit code ${code}: ${pkgManager} ${args.join(" ")}`;
+ rej(new Error(errorMessage));
+ }
+ });
+ });
+
+ return spinner;
+};
+
+const runInstallCommand = async (pkgManager: PackageManager, projectDir: string): Promise => {
+ switch (pkgManager) {
+ // When using npm, inherit the stderr stream so that the progress bar is shown
+ case "npm":
+ await execa(pkgManager, ["install"], {
+ cwd: projectDir,
+ stderr: "inherit",
+ });
+
+ return null;
+ // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress
+ case "pnpm":
+ return execWithSpinner(projectDir, pkgManager, {
+ onDataHandle: (spinner) => (data) => {
+ const text = data.toString();
+
+ if (text.includes("Progress")) {
+ spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text;
+ }
+ },
+ });
+ case "yarn":
+ return execWithSpinner(projectDir, pkgManager, {
+ onDataHandle: (spinner) => (data) => {
+ spinner.text = data.toString();
+ },
+ });
+ // When using bun, the stdout stream is ignored and the spinner is shown
+ case "bun":
+ return execWithSpinner(projectDir, pkgManager, { stdout: "ignore" });
+ default:
+ throw new Error(`Unknown package manager: ${pkgManager}`);
+ }
+};
+
+export const installDependencies = async (args?: { projectDir?: string }) => {
+ const { projectDir = state.projectDir } = args ?? {};
+ logger.info("Installing dependencies...");
+ const pkgManager = getUserPkgManager();
+
+ const installSpinner = await runInstallCommand(pkgManager, projectDir);
+
+ // If the spinner was used to show the progress, use succeed method on it
+ // If not, use the succeed on a new spinner
+ (installSpinner ?? ora()).succeed(chalk.green("Successfully installed dependencies!\n"));
+};
+
+export const runExecCommand = async ({
+ command,
+ projectDir = state.projectDir,
+ successMessage,
+ errorMessage,
+ loadingMessage,
+}: {
+ command: string[];
+ projectDir?: string;
+ successMessage?: string;
+ errorMessage?: string;
+ loadingMessage?: string;
+}) => {
+ let spinner: Ora | null = null;
+
+ try {
+ spinner = await _runExecCommand({
+ projectDir,
+ command,
+ loadingMessage,
+ });
+
+ // If the spinner was used to show the progress, use succeed method on it
+ // If not, use the succeed on a new spinner
+ (spinner ?? ora()).succeed(
+ chalk.green(successMessage ? `${successMessage}\n` : `Successfully ran ${command.join(" ")}!\n`),
+ );
+ } catch (error) {
+ // If we have a spinner, fail it, otherwise just throw the error
+ if (spinner) {
+ const failMessage = errorMessage || `Failed to run ${command.join(" ")}`;
+ spinner.fail(chalk.red(failMessage));
+ }
+ throw error;
+ }
+};
+
+export const _runExecCommand = async ({
+ projectDir,
+ command,
+ loadingMessage,
+}: {
+ projectDir: string;
+ exec?: boolean;
+ command: string[];
+ loadingMessage?: string;
+}): Promise => {
+ const pkgManager = getUserPkgManager();
+ switch (pkgManager) {
+ // When using npm, capture both stdout and stderr to show error messages
+ case "npm": {
+ const result = await execa("npx", [...command], {
+ cwd: projectDir,
+ stdout: "pipe",
+ stderr: "pipe",
+ reject: false,
+ });
+
+ if (result.exitCode !== 0) {
+ // Combine stdout and stderr for complete error message
+ const combinedOutput = [result.stdout, result.stderr]
+ .filter((output) => output?.trim())
+ .join("\n")
+ .trim()
+ // Remove spinner-related lines that aren't useful in error output
+ .replace(/^- Checking registry\.$/gm, "")
+ .replace(/^\s*$/gm, "") // Remove empty lines
+ .trim();
+
+ const errorMessage =
+ combinedOutput || `Command failed with exit code ${result.exitCode}: npx ${command.join(" ")}`;
+ throw new Error(errorMessage);
+ }
+
+ return null;
+ }
+ // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress
+ case "pnpm": {
+ // For shadcn commands, don't use progress handler to capture full output
+ const isInstallCommand = command.includes("install");
+ return execWithSpinner(projectDir, "pnpm", {
+ args: ["dlx", ...command],
+ loadingMessage,
+ onDataHandle: isInstallCommand
+ ? (spinner) => (data) => {
+ const text = data.toString();
+
+ if (text.includes("Progress")) {
+ spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text;
+ }
+ }
+ : undefined,
+ });
+ }
+ case "yarn": {
+ // For shadcn commands, don't use progress handler to capture full output
+ const isYarnInstallCommand = command.includes("install");
+ return execWithSpinner(projectDir, pkgManager, {
+ args: [...command],
+ loadingMessage,
+ onDataHandle: isYarnInstallCommand
+ ? (spinner) => (data) => {
+ spinner.text = data.toString();
+ }
+ : undefined,
+ });
+ }
+ // When using bun, the stdout stream is ignored and the spinner is shown
+ case "bun":
+ return execWithSpinner(projectDir, "bunx", {
+ stdout: "ignore",
+ args: [...command],
+ loadingMessage,
+ });
+ default:
+ throw new Error(`Unknown package manager: ${pkgManager}`);
+ }
+};
+
+export function generateRandomSecret(): string {
+ return crypto.randomUUID().replace(/-/g, "");
+}
diff --git a/packages/cli-old/src/helpers/installPackages.ts b/packages/cli-old/src/helpers/installPackages.ts
new file mode 100644
index 00000000..06345c47
--- /dev/null
+++ b/packages/cli-old/src/helpers/installPackages.ts
@@ -0,0 +1,25 @@
+import type { InstallerOptions, PkgInstallerMap } from "~/installers/index.js";
+import { logger } from "~/utils/logger.js";
+
+type InstallPackagesOptions = InstallerOptions & {
+ packages: PkgInstallerMap;
+};
+// This runs the installer for all the packages that the user has selected
+export const installPackages = (options: InstallPackagesOptions) => {
+ const { packages } = options;
+ logger.info("Adding boilerplate...");
+
+ for (const [_name, pkgOpts] of Object.entries(packages)) {
+ if (pkgOpts.inUse) {
+ // const spinner = ora(`Boilerplating ${name}...`).start();
+ pkgOpts.installer(options);
+ // spinner.succeed(
+ // chalk.green(
+ // `Successfully setup boilerplate for ${chalk.green.bold(name)}`
+ // )
+ // );
+ }
+ }
+
+ logger.info("");
+};
diff --git a/packages/cli-old/src/helpers/logNextSteps.ts b/packages/cli-old/src/helpers/logNextSteps.ts
new file mode 100644
index 00000000..5b7c845d
--- /dev/null
+++ b/packages/cli-old/src/helpers/logNextSteps.ts
@@ -0,0 +1,48 @@
+import chalk from "chalk";
+
+import { DEFAULT_APP_NAME } from "~/consts.js";
+import type { InstallerOptions } from "~/installers/index.js";
+import { state } from "~/state.js";
+import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
+import { logger } from "~/utils/logger.js";
+
+const formatRunCommand = (pkgManager: ReturnType, command: string) =>
+ ["npm", "bun"].includes(pkgManager) ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`;
+
+// This logs the next steps that the user should take in order to advance the project
+export const logNextSteps = ({
+ projectName = DEFAULT_APP_NAME,
+ noInstall,
+}: Pick) => {
+ const pkgManager = getUserPkgManager();
+
+ logger.info(chalk.bold("Next steps:"));
+ logger.dim("\nNavigate to the project directory:");
+ projectName !== "." && logger.info(` cd ${projectName}`);
+ logger.dim("(or open in your code editor, and run the rest of these commands from there)");
+
+ if (noInstall) {
+ logger.dim("\nInstall dependencies:");
+ // To reflect yarn's default behavior of installing packages when no additional args provided
+ if (pkgManager === "yarn") {
+ logger.info(` ${pkgManager}`);
+ } else {
+ logger.info(` ${pkgManager} install`);
+ }
+ }
+
+ logger.dim("\nStart the dev server to view your app in a browser:");
+ logger.info(` ${formatRunCommand(pkgManager, "dev")}`);
+
+ if (state.appType === "webviewer") {
+ logger.dim("\nWhen you're ready to generate FileMaker clients:");
+ logger.info(` ${formatRunCommand(pkgManager, "typegen")}`);
+
+ logger.dim("\nTo open the starter inside FileMaker once your file is ready:");
+ logger.info(` ${formatRunCommand(pkgManager, "launch-fm")}`);
+ }
+
+ logger.dim("\nOr, run the ProofKit command again to add more to your project:");
+ logger.info(` ${formatRunCommand(pkgManager, "proofkit")}`);
+ logger.dim("(Must be inside the project directory)");
+};
diff --git a/packages/cli-old/src/helpers/replaceText.ts b/packages/cli-old/src/helpers/replaceText.ts
new file mode 100644
index 00000000..e7f9d4b1
--- /dev/null
+++ b/packages/cli-old/src/helpers/replaceText.ts
@@ -0,0 +1,17 @@
+import fs from "node:fs";
+import path from "node:path";
+
+export function replaceTextInFiles(directoryPath: string, search: string, replacement: string): void {
+ const files = fs.readdirSync(directoryPath);
+
+ for (const file of files) {
+ const filePath = path.join(directoryPath, file);
+ if (fs.statSync(filePath).isDirectory()) {
+ replaceTextInFiles(filePath, search, replacement);
+ } else {
+ const data = fs.readFileSync(filePath, "utf8");
+ const updatedData = data.replace(new RegExp(search, "g"), replacement);
+ fs.writeFileSync(filePath, updatedData, "utf8");
+ }
+ }
+}
diff --git a/packages/cli-old/src/helpers/scaffoldProject.ts b/packages/cli-old/src/helpers/scaffoldProject.ts
new file mode 100644
index 00000000..7905eb0a
--- /dev/null
+++ b/packages/cli-old/src/helpers/scaffoldProject.ts
@@ -0,0 +1,136 @@
+import path from "node:path";
+import chalk from "chalk";
+import fs from "fs-extra";
+import ora from "ora";
+import * as p from "~/cli/prompts.js";
+
+import { PKG_ROOT } from "~/consts.js";
+import type { InstallerOptions } from "~/installers/index.js";
+import { isNonInteractiveMode, state } from "~/state.js";
+import { logger } from "~/utils/logger.js";
+
+const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]);
+
+function getMeaningfulDirectoryEntries(projectDir: string): string[] {
+ return fs.readdirSync(projectDir).filter((entry) => {
+ if (AGENT_METADATA_DIRS.has(entry)) {
+ return false;
+ }
+
+ if (entry === ".gitignore") {
+ return true;
+ }
+
+ if (entry.startsWith(".")) {
+ return false;
+ }
+
+ return true;
+ });
+}
+
+// This bootstraps the base Next.js application
+export const scaffoldProject = async ({
+ projectName,
+ pkgManager,
+ noInstall,
+ force = false,
+}: InstallerOptions & { force?: boolean }) => {
+ const projectDir = state.projectDir;
+
+ const srcDir = path.join(
+ PKG_ROOT,
+ state.appType === "browser"
+ ? `template/${state.ui === "mantine" ? "nextjs-mantine" : "nextjs-shadcn"}`
+ : "template/vite-wv",
+ );
+
+ if (noInstall) {
+ logger.info("");
+ } else {
+ logger.info(`\nUsing: ${chalk.cyan.bold(pkgManager)}\n`);
+ }
+
+ const spinner = ora(`Scaffolding in: ${projectDir}...\n`).start();
+
+ if (fs.existsSync(projectDir)) {
+ const meaningfulEntries = getMeaningfulDirectoryEntries(projectDir);
+
+ if (meaningfulEntries.length === 0) {
+ if (projectName !== ".") {
+ spinner.info(`${chalk.cyan.bold(projectName)} exists but is empty, continuing...\n`);
+ }
+ } else if (force) {
+ spinner.info(
+ `${chalk.yellow("Force mode enabled:")} clearing ${chalk.cyan.bold(projectName)} before scaffolding...\n`,
+ );
+ fs.emptyDirSync(projectDir);
+ spinner.start();
+ // continue to scaffold after clearing
+ } else if (isNonInteractiveMode()) {
+ spinner.fail(
+ `${chalk.redBright.bold("Error:")} ${chalk.cyan.bold(
+ projectName,
+ )} already exists and isn't empty. Remove the existing files or choose a different directory.`,
+ );
+ throw new Error(
+ `Cannot initialize into a non-empty directory in non-interactive mode: ${meaningfulEntries.join(", ")}`,
+ );
+ } else {
+ spinner.stopAndPersist();
+ const overwriteDir = await p.select({
+ message: `${chalk.redBright.bold("Warning:")} ${chalk.cyan.bold(
+ projectName,
+ )} already exists and isn't empty. How would you like to proceed?`,
+ options: [
+ {
+ label: "Abort installation (recommended)",
+ value: "abort",
+ },
+ {
+ label: "Clear the directory and continue installation",
+ value: "clear",
+ },
+ {
+ label: "Continue installation and overwrite conflicting files",
+ value: "overwrite",
+ },
+ ],
+ initialValue: "abort",
+ });
+ if (overwriteDir === "abort") {
+ spinner.fail("Aborting installation...");
+ process.exit(1);
+ }
+
+ const overwriteAction = overwriteDir === "clear" ? "clear the directory" : "overwrite conflicting files";
+
+ const confirmOverwriteDir = await p.confirm({
+ message: `Are you sure you want to ${overwriteAction}?`,
+ initialValue: false,
+ });
+
+ if (!confirmOverwriteDir) {
+ spinner.fail("Aborting installation...");
+ process.exit(1);
+ }
+
+ if (overwriteDir === "clear") {
+ spinner.info(`Emptying ${chalk.cyan.bold(projectName)} and creating new ProofKit app..\n`);
+ fs.emptyDirSync(projectDir);
+ }
+ }
+ }
+
+ spinner.start();
+
+ // Copy the main template
+ fs.copySync(srcDir, projectDir);
+
+ // Rename gitignore
+ fs.renameSync(path.join(projectDir, "_gitignore"), path.join(projectDir, ".gitignore"));
+
+ const scaffoldedName = projectName === "." ? "App" : chalk.cyan.bold(projectName);
+
+ spinner.succeed(`${scaffoldedName} ${chalk.green("scaffolded successfully!")}\n`);
+};
diff --git a/packages/cli-old/src/helpers/selectBoilerplate.ts b/packages/cli-old/src/helpers/selectBoilerplate.ts
new file mode 100644
index 00000000..4b538d3d
--- /dev/null
+++ b/packages/cli-old/src/helpers/selectBoilerplate.ts
@@ -0,0 +1,32 @@
+import path from "node:path";
+import fs from "fs-extra";
+
+import { PKG_ROOT } from "~/consts.js";
+import type { InstallerOptions } from "~/installers/index.js";
+import { state } from "~/state.js";
+
+type SelectBoilerplateProps = Required>;
+
+export const selectLayoutFile = (_props: SelectBoilerplateProps) => {
+ const projectDir = state.projectDir;
+ const layoutFileDir = path.join(PKG_ROOT, "template/extras/src/app/layout");
+
+ const layoutFile = "base.tsx";
+
+ const appSrc = path.join(layoutFileDir, layoutFile); // base layout
+ const appDest = path.join(projectDir, "src/app/layout.tsx");
+ fs.copySync(appSrc, appDest);
+
+ fs.copySync(path.join(layoutFileDir, "main-shell.tsx"), path.join(projectDir, "src/app/(main)/layout.tsx"));
+};
+
+export const selectPageFile = (_props: SelectBoilerplateProps) => {
+ const projectDir = state.projectDir;
+ const indexFileDir = path.join(PKG_ROOT, "template/extras/src/app/page");
+
+ const indexFile = "base.tsx";
+
+ const indexSrc = path.join(indexFileDir, indexFile);
+ const indexDest = path.join(projectDir, "src/app/(main)/page.tsx");
+ fs.copySync(indexSrc, indexDest);
+};
diff --git a/packages/cli-old/src/helpers/setImportAlias.ts b/packages/cli-old/src/helpers/setImportAlias.ts
new file mode 100644
index 00000000..7551134b
--- /dev/null
+++ b/packages/cli-old/src/helpers/setImportAlias.ts
@@ -0,0 +1,12 @@
+import { replaceTextInFiles } from "./replaceText.js";
+
+const TRAILING_SLASH_REGEX = /[^/]$/;
+
+export const setImportAlias = (projectDir: string, importAlias: string) => {
+ const normalizedImportAlias = importAlias
+ .replace(/\*/g, "") // remove any wildcards (~/* -> ~/)
+ .replace(TRAILING_SLASH_REGEX, "$&/"); // ensure trailing slash (@ -> ~/)
+
+ // update import alias in any files if not using the default
+ replaceTextInFiles(projectDir, "~/", normalizedImportAlias);
+};
diff --git a/packages/cli-old/src/helpers/shadcn-cli.ts b/packages/cli-old/src/helpers/shadcn-cli.ts
new file mode 100644
index 00000000..4c235380
--- /dev/null
+++ b/packages/cli-old/src/helpers/shadcn-cli.ts
@@ -0,0 +1,80 @@
+import fs from "node:fs";
+import path from "node:path";
+import { execa } from "execa";
+
+import { DEFAULT_REGISTRY_URL } from "~/consts.js";
+import { state } from "~/state.js";
+import { logger } from "~/utils/logger.js";
+import { getSettings } from "~/utils/parseSettings.js";
+
+export async function shadcnInstall(components: string | string[], _friendlyComponentName?: string) {
+ const componentsArray = Array.isArray(components) ? components : [components];
+ const command = ["shadcn@latest", "add", ...componentsArray];
+ // Use execa to run the shadcn add command directly
+
+ try {
+ await execa("pnpm", ["dlx", ...command], {
+ stdio: "inherit",
+ cwd: state.projectDir ?? process.cwd(),
+ });
+ } catch (error) {
+ logger.error(`Failed to run shadcn add: ${error}`);
+ throw error;
+ }
+}
+
+export function getRegistryUrl(): string {
+ let url: string;
+ try {
+ url = getSettings().registryUrl ?? DEFAULT_REGISTRY_URL;
+ } catch {
+ // If we can't get settings (e.g., during development or outside a ProofKit project),
+ // fall back to the default registry URL
+ url = DEFAULT_REGISTRY_URL;
+ }
+ return url.endsWith("/") ? url.slice(0, -1) : url;
+}
+
+export interface ShadcnConfig {
+ style: "default" | "new-york";
+ tailwind: {
+ config: string;
+ css: string;
+ baseColor: string;
+ cssVariables: boolean;
+ prefix?: string;
+ [k: string]: unknown;
+ };
+ rsc: boolean;
+ tsx?: boolean;
+ iconLibrary?: string;
+ aliases: {
+ utils: string;
+ components: string;
+ ui?: string;
+ lib?: string;
+ hooks?: string;
+ [k: string]: unknown;
+ };
+ registries?: {
+ [k: string]:
+ | string
+ | {
+ url: string;
+ params?: {
+ [k: string]: string;
+ };
+ headers?: {
+ [k: string]: string;
+ };
+ [k: string]: unknown;
+ };
+ };
+ [k: string]: unknown;
+}
+
+export function getShadcnConfig() {
+ const componentsJsonPath = path.join(state.projectDir, "components.json");
+ const componentsJson = JSON.parse(fs.readFileSync(componentsJsonPath, "utf8"));
+ return componentsJson as ShadcnConfig;
+}
diff --git a/packages/cli-old/src/helpers/stealth-init.ts b/packages/cli-old/src/helpers/stealth-init.ts
new file mode 100644
index 00000000..6f865ab8
--- /dev/null
+++ b/packages/cli-old/src/helpers/stealth-init.ts
@@ -0,0 +1,20 @@
+import fs from "fs-extra";
+
+import { defaultSettings, setSettings, validateAndSetEnvFile } from "~/utils/parseSettings.js";
+
+/**
+ * Used to add a proofkit.json file to an existing project
+ */
+export async function stealthInit() {
+ // check if proofkit.json exists
+ const proofkitJson = await fs.pathExists("proofkit.json");
+ if (proofkitJson) {
+ return;
+ }
+
+ // create proofkit.json with default settings
+ setSettings(defaultSettings);
+
+ // validate and set envFile only if it exists
+ validateAndSetEnvFile();
+}
diff --git a/packages/cli-old/src/helpers/version-fetcher.ts b/packages/cli-old/src/helpers/version-fetcher.ts
new file mode 100644
index 00000000..26a21e80
--- /dev/null
+++ b/packages/cli-old/src/helpers/version-fetcher.ts
@@ -0,0 +1,131 @@
+import https from "node:https";
+import { TRPCError } from "@trpc/server";
+import axios from "axios";
+import z from "zod/v4";
+
+export async function fetchServerVersions({ url, ottoPort = 3030 }: { url: string; ottoPort?: number }) {
+ const fmsInfo = await fetchFMSVersionInfo(url);
+ const ottoInfo = await fetchOttoVersion({ url, ottoPort });
+ return { fmsInfo, ottoInfo };
+}
+
+const fmsInfoSchema = z.object({
+ data: z.object({
+ APIVersion: z.number().optional(),
+ AcceptEARPassword: z.boolean().optional(),
+ AcceptEncrypted: z.boolean().optional(),
+ AcceptUnencrypted: z.boolean().optional(),
+ AdminLocalAuth: z.string().optional(),
+ AllowChangeUploadDBFolder: z.boolean().optional(),
+ AutoOpenForUpload: z.boolean().optional(),
+ DenyGuestAndAutoLogin: z.string().optional(),
+ Hostname: z.string().optional(),
+ IsAppleInternal: z.boolean().optional(),
+ IsETS: z.boolean().optional(),
+ PremisesType: z.string().optional(),
+ ProductVersion: z.string().optional(),
+ PublicKey: z.string().optional(),
+ RequiresDBPasswords: z.boolean().optional(),
+ ServerID: z.string().optional(),
+ ServerVersion: z.string(),
+ }),
+ result: z.number(),
+});
+
+export async function fetchFMSVersionInfo(url: string) {
+ const fmsUrl = new URL(url);
+ fmsUrl.pathname = "/fmws/serverinfo";
+
+ const fmsInfoResult = await fetchWithoutSSL(fmsUrl.toString()).then((r) => fmsInfoSchema.safeParse(r.data));
+ if (!fmsInfoResult.success) {
+ console.error("fmsInfoResult.error", fmsInfoResult.error.issues);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Invalid FileMaker Server URL",
+ });
+ }
+ return fmsInfoResult.data.data;
+}
+
+const ottoInfoSchema = z.object({
+ Otto: z.object({
+ version: z.string(),
+ serverNickname: z.string().default(""),
+ isLicenseValid: z.boolean().optional(),
+ }),
+ migratorVersion: z.string().optional(),
+ FileMakerServer: z.object({
+ version: z.object({
+ long: z.string(),
+ short: z.string(),
+ }),
+ running: z.boolean().optional(),
+ }),
+ isMac: z.boolean().optional(),
+ platform: z.string().optional(),
+ host: z.string().optional(),
+});
+
+const ottoInfoResponseSchema = z.object({
+ response: ottoInfoSchema,
+});
+
+export async function fetchOttoVersion({
+ url,
+ ottoPort = 3030,
+}: {
+ url: string;
+ ottoPort?: number | null;
+}): Promise | null> {
+ let ottoInfo = await fetchOtto4Version(url);
+ if (!ottoInfo) {
+ ottoInfo = await fetchOtto3Version(url, ottoPort);
+ }
+ return ottoInfo;
+}
+
+async function fetchOtto4Version(url: string) {
+ try {
+ const otto4Url = new URL(url);
+ otto4Url.pathname = "/otto/api/info";
+ const otto4Info = await fetchWithoutSSL(otto4Url.toString()).then((r) => {
+ return ottoInfoResponseSchema.parse(r.data).response;
+ });
+ return otto4Info;
+ } catch (_error) {
+ console.log("unable to fetch otto4 info, trying otto3");
+ return null;
+ }
+}
+
+async function fetchOtto3Version(url: string, ottoPort: number | null) {
+ try {
+ const otto3Url = new URL(url);
+ otto3Url.port = ottoPort ? ottoPort.toString() : "3030";
+ otto3Url.pathname = "/api/otto/info";
+ const ottoInfo = await fetchWithoutSSL(otto3Url.toString()).then((res) => {
+ return ottoInfoSchema.parse(res.data);
+ });
+ return ottoInfo;
+ } catch (error) {
+ if (error instanceof Error) {
+ console.error("otto3 fetch error", error.message);
+ }
+ return null;
+ }
+}
+
+async function fetchWithoutSSL(url: string) {
+ const agent = new https.Agent({
+ rejectUnauthorized: false,
+ });
+
+ const result = await axios.get(url, {
+ validateStatus: null,
+ headers: { Connection: "close" },
+ httpsAgent: agent,
+ timeout: 10_000,
+ });
+
+ return result;
+}
diff --git a/packages/cli-old/src/index.ts b/packages/cli-old/src/index.ts
new file mode 100644
index 00000000..a61c41c7
--- /dev/null
+++ b/packages/cli-old/src/index.ts
@@ -0,0 +1,96 @@
+#!/usr/bin/env node --no-warnings
+import chalk from "chalk";
+import { Command } from "commander";
+import { makeInitCommand, runInit } from "~/cli/init.js";
+import { intro } from "~/cli/prompts.js";
+import { logger } from "~/utils/logger.js";
+import { proofGradient, renderTitle } from "~/utils/renderTitle.js";
+import { makeAddCommand } from "./cli/add/index.js";
+import { makeDeployCommand } from "./cli/deploy/index.js";
+import { runMenu } from "./cli/menu.js";
+import { makeRemoveCommand } from "./cli/remove/index.js";
+import { makeTypegenCommand } from "./cli/typegen/index.js";
+import { makeUpgradeCommand } from "./cli/update/makeUpgradeCommand.js";
+import { UserAbortedError } from "./cli/utils.js";
+import { npmName } from "./consts.js";
+import { ciOption, nonInteractiveOption } from "./globalOptions.js";
+import { initProgramState, isNonInteractiveMode } from "./state.js";
+import { getVersion } from "./utils/getProofKitVersion.js";
+import { getSettings, type Settings } from "./utils/parseSettings.js";
+import { checkAndRenderVersionWarning } from "./utils/renderVersionWarning.js";
+
+const version = getVersion();
+
+function getErrorMessage(error: unknown): string {
+ if (error instanceof Error) {
+ return error.message;
+ }
+ return String(error);
+}
+
+const main = async () => {
+ const program = new Command();
+ renderTitle();
+ if (process.env.PROOFKIT_SKIP_VERSION_CHECK !== "1") {
+ await checkAndRenderVersionWarning();
+ }
+
+ program
+ .name(npmName)
+ .version(version)
+ .command("default", { hidden: true, isDefault: true })
+ .addOption(ciOption)
+ .addOption(nonInteractiveOption)
+ .action(async (args) => {
+ initProgramState(args);
+
+ let settings: Settings | undefined;
+ try {
+ settings = getSettings();
+ } catch {
+ // void
+ }
+
+ if (isNonInteractiveMode()) {
+ throw new Error(
+ "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit init --non-interactive`.",
+ );
+ }
+
+ if (settings) {
+ intro(`Found ${proofGradient("ProofKit")} project`);
+ await runMenu();
+ } else {
+ intro(`No ${proofGradient("ProofKit")} project found, running \`init\``);
+ await runInit();
+ }
+ })
+ .addHelpText("afterAll", `\n The ProofKit CLI was inspired by the ${chalk.hex("#E8DCFF").bold("t3 stack")}\n`);
+
+ program.addCommand(makeInitCommand());
+ program.addCommand(makeAddCommand());
+ program.addCommand(makeRemoveCommand());
+ program.addCommand(makeTypegenCommand());
+ program.addCommand(makeDeployCommand());
+ program.addCommand(makeUpgradeCommand());
+
+ await program.parseAsync(process.argv);
+ process.exit(0);
+};
+
+main().catch((err) => {
+ if (err instanceof UserAbortedError) {
+ process.exit(0);
+ } else if (err instanceof Error) {
+ logger.error("Aborting installation...");
+ logger.error(err.message);
+ const cause = (err as Error & { cause?: unknown }).cause;
+ if (cause) {
+ logger.dim(`Cause: ${getErrorMessage(cause)}`);
+ }
+ } else {
+ logger.error("An unknown error has occurred. Please open an issue on github with the below:");
+ console.log(err);
+ }
+ process.exit(1);
+});
diff --git a/packages/cli-old/src/installers/auth-shared.ts b/packages/cli-old/src/installers/auth-shared.ts
new file mode 100644
index 00000000..20f1401d
--- /dev/null
+++ b/packages/cli-old/src/installers/auth-shared.ts
@@ -0,0 +1,49 @@
+import { type SourceFile, SyntaxKind } from "ts-morph";
+
+import { ensureReturnStatementIsWrappedInFragment } from "~/utils/ts-morph.js";
+
+export function addToHeaderSlot(slotSourceFile: SourceFile, importFrom: string) {
+ slotSourceFile.addImportDeclaration({
+ defaultImport: "UserMenu",
+ moduleSpecifier: importFrom,
+ });
+
+ // ensure Group from @mantine/core is imported
+ const mantineCoreImport = slotSourceFile.getImportDeclaration(
+ (dec) => dec.getModuleSpecifierValue() === "@mantine/core",
+ );
+ if (mantineCoreImport) {
+ const groupImport = mantineCoreImport.getNamedImports().find((imp) => imp.getName() === "Group");
+
+ if (!groupImport) {
+ mantineCoreImport.addNamedImport({ name: "Group" });
+ }
+ } else {
+ slotSourceFile.addImportDeclaration({
+ namedImports: [{ name: "Group" }],
+ moduleSpecifier: "@mantine/core",
+ });
+ }
+
+ const returnStatement = ensureReturnStatementIsWrappedInFragment(
+ slotSourceFile
+ .getFunction((dec) => dec.isDefaultExport())
+ ?.getBody()
+ ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement),
+ );
+
+ const existingElements = returnStatement
+ ?.getFirstDescendantByKind(SyntaxKind.JsxOpeningFragment)
+ ?.getParentIfKind(SyntaxKind.JsxFragment)
+ ?.getFirstDescendantByKind(SyntaxKind.SyntaxList)
+ ?.getText();
+
+ if (!existingElements) {
+ console.log(`Failed to inject into header slot at ${slotSourceFile.getFilePath()}`);
+ return;
+ }
+
+ returnStatement?.replaceWithText(`return (<>${existingElements}>)`);
+ returnStatement?.formatText();
+ slotSourceFile.saveSync();
+}
diff --git a/packages/cli-old/src/installers/better-auth.ts b/packages/cli-old/src/installers/better-auth.ts
new file mode 100644
index 00000000..f417ab36
--- /dev/null
+++ b/packages/cli-old/src/installers/better-auth.ts
@@ -0,0 +1,3 @@
+export async function betterAuthInstaller() {
+ // TODO: Implement better-auth installer
+}
diff --git a/packages/cli-old/src/installers/clerk.ts b/packages/cli-old/src/installers/clerk.ts
new file mode 100644
index 00000000..11ddd816
--- /dev/null
+++ b/packages/cli-old/src/installers/clerk.ts
@@ -0,0 +1,153 @@
+import path from "node:path";
+import chalk from "chalk";
+import fs from "fs-extra";
+import { type SourceFile, SyntaxKind } from "ts-morph";
+
+import { PKG_ROOT } from "~/consts.js";
+import { addPackageDependency } from "~/utils/addPackageDependency.js";
+import { addToEnv } from "~/utils/addToEnvs.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+import { addToHeaderSlot } from "./auth-shared.js";
+
+export const clerkInstaller = async ({ projectDir }: { projectDir: string }) => {
+ addPackageDependency({
+ projectDir,
+ dependencies: ["@clerk/nextjs", "@clerk/themes"],
+ devMode: false,
+ });
+
+ // add clerk middleware
+ // check if middleware already exists, if not add it
+ const extrasDir = path.join(PKG_ROOT, "template/extras");
+
+ const middlewareDest = path.join(projectDir, "src/middleware.ts");
+ if (fs.existsSync(middlewareDest)) {
+ // throw new Error("Middleware already exists");
+ console.log(
+ chalk.yellow(
+ "Middleware already exists. To require auth for your app, be sure to follow the guide to setup Clerk middleware. https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-next-js",
+ ),
+ );
+ } else {
+ const middlewareSrc = path.join(extrasDir, "src/middleware/clerk.ts");
+ fs.copySync(middlewareSrc, middlewareDest);
+ }
+
+ // copy auth pages
+ fs.copySync(path.join(extrasDir, "src/app/clerk-auth"), path.join(projectDir, "src/app/auth"));
+
+ // copy auth components
+ fs.copySync(path.join(extrasDir, "src/components/clerk-auth"), path.join(projectDir, "src/components/clerk-auth"));
+
+ // add ClerkProvider to app layout
+ const layoutFile = path.join(projectDir, "src/app/layout.tsx");
+ const project = getNewProject(projectDir);
+ addClerkProvider(project.addSourceFileAtPath(layoutFile));
+
+ // inject signin/signout components to header slots
+ addToHeaderSlot(
+ project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")),
+ "@/components/clerk-auth/user-menu",
+ );
+ addToHeaderSlot(
+ project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-mobile-content.tsx")),
+ "@/components/clerk-auth/user-menu-mobile",
+ );
+
+ addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts")));
+
+ // add envs to .env and .env.schema
+ await addToEnv({
+ projectDir,
+ project,
+ envs: [
+ {
+ name: "NEXT_PUBLIC_CLERK_SIGN_IN_URL",
+ zodValue: "z.string()",
+ defaultValue: "/auth/signin",
+ type: "client",
+ },
+ {
+ name: "NEXT_PUBLIC_CLERK_SIGN_UP_URL",
+ zodValue: "z.string()",
+ defaultValue: "/auth/signup",
+ type: "client",
+ },
+ {
+ name: "CLERK_SECRET_KEY",
+ zodValue: `z.string().startsWith('sk_').min(1, {
+ message:
+ "No Clerk Secret Key found. Did you create your Clerk app and copy the environment variables to you .env file?",
+ })`,
+ type: "server",
+ },
+ {
+ name: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
+ zodValue: `z.string().startsWith('pk_').min(1, {
+ message:
+ "No Clerk Public Key found. Did you create your Clerk app and copy the environment variables to you .env file?",
+ })`,
+ type: "client",
+ },
+ ],
+ envFileDescription:
+ "Hosted auth with Clerk. Set up a new app at https://dashboard.clerk.com/apps/new to get these values.",
+ });
+
+ await formatAndSaveSourceFiles(project);
+};
+
+export function addClerkProvider(sourceFile: SourceFile) {
+ sourceFile.addImportDeclaration({
+ namedImports: [{ name: "ClerkAuthProvider" }],
+ moduleSpecifier: "@/components/clerk-auth/clerk-provider",
+ });
+
+ // Step 2: Wrap default exported function's return statement with ClerkProvider
+ const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport());
+
+ // find the mantine provider in this export
+ const mantineProvider = exportDefault
+ ?.getBody()
+ ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement)
+ ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
+ .find((openingElement) => openingElement.getTagNameNode().getText() === "MantineProvider")
+ ?.getParentIfKind(SyntaxKind.JsxElement);
+
+ const childrenText = mantineProvider
+ ?.getJsxChildren()
+ .map((child) => child.getText())
+ .filter(Boolean)
+ .join("\n");
+
+ mantineProvider?.getChildSyntaxList()?.replaceWithText(
+ `
+ ${childrenText}
+ `,
+ );
+}
+
+function addToSafeActionClient(sourceFile?: SourceFile) {
+ if (!sourceFile) {
+ console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?"));
+ return;
+ }
+
+ sourceFile.addImportDeclaration({
+ namedImports: [{ name: "auth", alias: "getAuth" }],
+ moduleSpecifier: "@clerk/nextjs/server",
+ });
+
+ // add to end of file
+ sourceFile.addStatements((writer) =>
+ writer.writeLine(`export const authedActionClient = actionClient.use(async ({ next, ctx }) => {
+ const auth = getAuth();
+ if (!auth.userId) {
+ throw new Error("Unauthorized");
+ }
+ return next({ ctx: { ...ctx, auth } });
+});
+
+`),
+ );
+}
diff --git a/packages/cli-old/src/installers/dependencyVersionMap.ts b/packages/cli-old/src/installers/dependencyVersionMap.ts
new file mode 100644
index 00000000..c5b53268
--- /dev/null
+++ b/packages/cli-old/src/installers/dependencyVersionMap.ts
@@ -0,0 +1,108 @@
+import { getNodeMajorVersion } from "~/utils/getProofKitVersion.js";
+import { getProofkitReleaseTag } from "~/utils/proofkitReleaseChannel.js";
+
+const proofkitReleaseTag = getProofkitReleaseTag();
+
+/*
+ * This maps the necessary packages to a version.
+ * This improves performance significantly over fetching it from the npm registry.
+ */
+export const dependencyVersionMap = {
+ // Resolve to "latest" or "beta" based on current changeset state / versions.
+ "@proofkit/fmdapi": proofkitReleaseTag,
+ "@proofkit/webviewer": proofkitReleaseTag,
+ "@proofkit/cli": proofkitReleaseTag,
+ "@proofkit/typegen": proofkitReleaseTag,
+ "@proofkit/better-auth": proofkitReleaseTag,
+
+ // NextAuth.js
+ "next-auth": "beta",
+ "next-auth-adapter-filemaker": "beta",
+
+ "@auth/prisma-adapter": "^1.6.0",
+ "@auth/drizzle-adapter": "^1.1.0",
+
+ // Prisma
+ prisma: "^5.14.0",
+ "@prisma/client": "^5.14.0",
+ "@prisma/adapter-planetscale": "^5.14.0",
+
+ // Drizzle
+ "drizzle-orm": "^0.30.10",
+ "drizzle-kit": "^0.21.4",
+ mysql2: "^3.9.7",
+ "@planetscale/database": "^1.18.0",
+ postgres: "^3.4.4",
+ "@libsql/client": "^0.6.0",
+
+ // TailwindCSS
+ tailwindcss: "^4.1.10",
+ postcss: "^8.4.41",
+ "@tailwindcss/postcss": "^4.1.10",
+ "@tailwindcss/vite": "^4.2.1",
+ "class-variance-authority": "^0.7.1",
+ clsx: "^2.1.1",
+ "tailwind-merge": "^3.5.0",
+ "tw-animate-css": "^1.4.0",
+
+ // tRPC
+ "@trpc/client": "^11.0.0-rc.446",
+ "@trpc/server": "^11.0.0-rc.446",
+ "@trpc/react-query": "^11.0.0-rc.446",
+ "@trpc/next": "^11.0.0-rc.446",
+ superjson: "^2.2.1",
+ "server-only": "^0.0.1",
+
+ // Clerk
+ "@clerk/nextjs": "^6.3.1",
+ "@clerk/themes": "^2.1.33",
+
+ // Tanstack Query
+ "@tanstack/react-query": "^5.59.0",
+ "@tanstack/react-query-devtools": "^5.59.0",
+
+ // ProofKit Auth
+ "@node-rs/argon2": "^2.0.2",
+ "@oslojs/binary": "^1.0.0",
+ "@oslojs/crypto": "^1.0.1",
+ "@oslojs/encoding": "^1.1.0",
+ "js-cookie": "^3.0.5",
+ "@types/js-cookie": "^3.0.6",
+
+ // React Email
+ "@react-email/components": "^0.5.0",
+ "@react-email/render": "1.2.0",
+ "@react-email/preview-server": "^4.2.8",
+ "@plunk/node": "^3.0.3",
+ "react-email": "^4.2.8",
+ resend: "^4.0.0",
+ "@sendgrid/mail": "^8.1.4",
+
+ // Node
+ "@types/node": `^${getNodeMajorVersion()}`,
+
+ // Radix (for shadcn/ui)
+ "@radix-ui/react-slot": "^1.2.3",
+
+ // Icons (for shadcn/ui)
+ "lucide-react": "^0.577.0",
+
+ // better-auth
+ "better-auth": "^1.3.4",
+ "@daveyplate/better-auth-ui": "^2.1.3",
+
+ // Mantine UI
+ "@mantine/core": "^7.15.0",
+ "@mantine/dates": "^7.15.0",
+ "@mantine/hooks": "^7.15.0",
+ "@mantine/modals": "^7.15.0",
+ "@mantine/notifications": "^7.15.0",
+ "mantine-react-table": "^2.0.0",
+
+ // Theme utilities
+ "next-themes": "^0.4.6",
+
+ // Zod
+ zod: "^4",
+} as const;
+export type AvailableDependencies = keyof typeof dependencyVersionMap;
diff --git a/packages/cli-old/src/installers/envVars.ts b/packages/cli-old/src/installers/envVars.ts
new file mode 100644
index 00000000..2eb95da9
--- /dev/null
+++ b/packages/cli-old/src/installers/envVars.ts
@@ -0,0 +1,43 @@
+import path from "node:path";
+import fs from "fs-extra";
+
+import type { Installer } from "~/installers/index.js";
+import { state } from "~/state.js";
+import { logger } from "~/utils/logger.js";
+
+export type FMAuthKeys = { username: string; password: string } | { ottoApiKey: string };
+
+export const initEnvFile: Installer = () => {
+ const envFilePath = findT3EnvFile(false) ?? "./src/config/env.ts";
+
+ const envContent = `
+# When adding additional environment variables, the schema in "${envFilePath}"
+# should be updated accordingly.
+
+`
+ .trim()
+ .concat("\n");
+
+ const envDest = path.join(state.projectDir, ".env");
+
+ fs.writeFileSync(envDest, envContent, "utf-8");
+};
+export function findT3EnvFile(throwIfNotFound: false): string | null;
+export function findT3EnvFile(throwIfNotFound?: true): string;
+export function findT3EnvFile(throwIfNotFound?: boolean): string | null {
+ const possiblePaths = ["src/config/env.ts", "src/lib/env.ts", "src/env.ts", "lib/env.ts", "env.ts", "config/env.ts"];
+
+ for (const testPath of possiblePaths) {
+ const fullPath = path.join(state.projectDir, testPath);
+ if (fs.existsSync(fullPath)) {
+ return fullPath;
+ }
+ }
+
+ if (throwIfNotFound === false) {
+ return null;
+ }
+
+ logger.warn(`Could not find T3 env files. Run "proofkit add utils/t3-env" to initialize them.`);
+ throw new Error("T3 env file not found");
+}
diff --git a/packages/cli-old/src/installers/index.ts b/packages/cli-old/src/installers/index.ts
new file mode 100644
index 00000000..b4fb6fb6
--- /dev/null
+++ b/packages/cli-old/src/installers/index.ts
@@ -0,0 +1,31 @@
+import { initEnvFile } from "~/installers/envVars.js";
+import type { PackageManager } from "~/utils/getUserPkgManager.js";
+
+// Turning this into a const allows the list to be iterated over for programmatically creating prompt options
+// Should increase extensibility in the future
+export const availablePackages = ["nextAuth", "trpc", "envVariables", "fmdapi", "webViewerFetch", "clerk"] as const;
+export type AvailablePackages = (typeof availablePackages)[number];
+
+export interface InstallerOptions {
+ pkgManager: PackageManager;
+ noInstall: boolean;
+ packages?: PkgInstallerMap;
+ projectName: string;
+ scopedAppName: string;
+}
+
+export type Installer = (opts: InstallerOptions) => void;
+
+export type PkgInstallerMap = {
+ [pkg in AvailablePackages]?: {
+ inUse: boolean;
+ installer: Installer;
+ };
+};
+
+export const buildPkgInstallerMap = (): PkgInstallerMap => ({
+ envVariables: {
+ inUse: true,
+ installer: initEnvFile,
+ },
+});
diff --git a/packages/cli-old/src/installers/install-fm-addon.ts b/packages/cli-old/src/installers/install-fm-addon.ts
new file mode 100644
index 00000000..384c3e16
--- /dev/null
+++ b/packages/cli-old/src/installers/install-fm-addon.ts
@@ -0,0 +1,53 @@
+import os from "node:os";
+import path from "node:path";
+import chalk from "chalk";
+import fs from "fs-extra";
+
+import { PKG_ROOT } from "~/consts.js";
+import { logger } from "~/utils/logger.js";
+
+export async function installFmAddon({ addonName }: { addonName: "auth" | "wv" }) {
+ const addonDisplayName = addonName === "auth" ? "FM Auth Add-on" : "ProofKit WebViewer";
+
+ let targetDir: string | null = null;
+ if (process.platform === "win32") {
+ targetDir = path.join(os.homedir(), "AppData", "Local", "FileMaker", "Extensions", "AddonModules");
+ } else if (process.platform === "darwin") {
+ targetDir = path.join(os.homedir(), "Library", "Application Support", "FileMaker", "Extensions", "AddonModules");
+ }
+
+ if (!targetDir) {
+ logger.warn(`Could not install the ${addonDisplayName} addon. You will need to do this manually.`);
+ return;
+ }
+
+ const addonDir = addonName === "auth" ? "ProofKitAuth" : "ProofKitWV";
+
+ await fs.copy(path.join(PKG_ROOT, `template/fm-addon/${addonDir}`), path.join(targetDir, addonDir), {
+ overwrite: true,
+ });
+
+ console.log("");
+ console.log(chalk.bgYellow(" ACTION REQUIRED: "));
+ if (addonName === "auth") {
+ console.log(
+ `${chalk.yellowBright(
+ "You must install the FM Auth addon in your FileMaker file to continue.",
+ )} ${chalk.dim("(Learn more: https://proofkit.dev/auth/fm-addon)")}`,
+ );
+ } else {
+ console.log(
+ `${chalk.yellowBright(
+ "You must install the ProofKit WebViewer addon in your FileMaker file to continue.",
+ )} ${chalk.dim("(Learn more: https://proofkit.dev/webviewer)")}`,
+ );
+ }
+ const steps = [
+ "Restart FileMaker Pro (if it's currently running)",
+ `Open your FileMaker file, go to layout mode, and install the ${addonDisplayName} addon to the file`,
+ "Come back here to continue the installation",
+ ];
+ steps.forEach((step, index) => {
+ console.log(`${index + 1}. ${step}`);
+ });
+}
diff --git a/packages/cli-old/src/installers/nextAuth.ts b/packages/cli-old/src/installers/nextAuth.ts
new file mode 100644
index 00000000..793f6d80
--- /dev/null
+++ b/packages/cli-old/src/installers/nextAuth.ts
@@ -0,0 +1,189 @@
+import path from "node:path";
+import chalk from "chalk";
+import fs from "fs-extra";
+import ora from "ora";
+import { type SourceFile, SyntaxKind } from "ts-morph";
+
+import { PKG_ROOT } from "~/consts.js";
+import { getExistingSchemas } from "~/generators/fmdapi.js";
+import { _runExecCommand, generateRandomSecret } from "~/helpers/installDependencies.js";
+import { addPackageDependency } from "~/utils/addPackageDependency.js";
+import { addToEnv } from "~/utils/addToEnvs.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+import { addToHeaderSlot } from "./auth-shared.js";
+import { dependencyVersionMap } from "./dependencyVersionMap.js";
+
+export const nextAuthInstaller = async ({ projectDir }: { projectDir: string }) => {
+ addPackageDependency({
+ projectDir,
+ dependencies: ["next-auth", "next-auth-adapter-filemaker"],
+ devMode: false,
+ });
+
+ const extrasDir = path.join(PKG_ROOT, "template/extras");
+
+ const routeHandlerFile = "src/app/api/auth/[...nextauth]/route.ts";
+ const srcToUse = routeHandlerFile;
+
+ const apiHandlerSrc = path.join(extrasDir, srcToUse);
+ const apiHandlerDest = path.join(projectDir, srcToUse);
+ fs.copySync(apiHandlerSrc, apiHandlerDest);
+
+ const authConfigSrc = path.join(extrasDir, "src/server", "next-auth", "base.ts");
+ const authConfigDest = path.join(projectDir, "src/server/auth.ts");
+ fs.copySync(authConfigSrc, authConfigDest);
+
+ const passwordSrc = path.join(extrasDir, "src/server", "next-auth", "password.ts");
+ const passwordDest = path.join(projectDir, "src/server/password.ts");
+ fs.copySync(passwordSrc, passwordDest);
+
+ // copy users.ts to data directory
+ fs.copySync(path.join(extrasDir, "src/server/data/users.ts"), path.join(projectDir, "src/server/data/users.ts"));
+
+ // copy auth pages
+ fs.copySync(path.join(extrasDir, "src/app/next-auth"), path.join(projectDir, "src/app/auth"));
+
+ // copy auth components
+ fs.copySync(path.join(extrasDir, "src/components/next-auth"), path.join(projectDir, "src/components/next-auth"));
+
+ const project = getNewProject(projectDir);
+
+ // modify root layout to wrap with session provider
+ addNextAuthProviderToRootLayout(project.addSourceFileAtPath(path.join(projectDir, "src/app/layout.tsx")));
+
+ // inject signin/signout components to header slots
+ addToHeaderSlot(
+ project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")),
+ "@/components/next-auth/user-menu",
+ );
+ addToHeaderSlot(
+ project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-mobile-content.tsx")),
+ "@/components/next-auth/user-menu-mobile",
+ );
+
+ // add a protected safe-action-client
+ addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts")));
+
+ // // TODO do this part in-house, maybe with execa directly
+ // await runExecCommand({
+ // command: ["auth", "secret"],
+ // projectDir,
+ // });
+
+ // add middleware
+ fs.copySync(path.join(extrasDir, "src/middleware/next-auth.ts"), path.join(projectDir, "src/middleware.ts"));
+
+ // add envs to .env and .env.schema
+ await addToEnv({
+ projectDir,
+ project,
+ envs: [
+ {
+ name: "AUTH_SECRET",
+ zodValue: "z.string().min(1)",
+ defaultValue: generateRandomSecret(),
+ type: "server",
+ },
+ ],
+ });
+
+ await checkForNextAuthLayouts(projectDir);
+
+ await formatAndSaveSourceFiles(project);
+};
+
+function addNextAuthProviderToRootLayout(rootLayoutSource: SourceFile) {
+ // Add imports
+ rootLayoutSource.addImportDeclaration({
+ namedImports: [{ name: "NextAuthProvider" }],
+ moduleSpecifier: "@/components/next-auth/next-auth-provider",
+ });
+ rootLayoutSource.addImportDeclaration({
+ namedImports: [{ name: "auth" }],
+ moduleSpecifier: "@/server/auth",
+ });
+
+ const exportDefault = rootLayoutSource.getFunction((dec) => dec.isDefaultExport());
+
+ // make the function async
+ exportDefault?.setIsAsync(true);
+
+ // get the session server-side
+ exportDefault?.getFirstDescendantByKind(SyntaxKind.Block)?.insertStatements(0, "const session = await auth();");
+
+ // get the body element from the return statement
+ const bodyElement = exportDefault
+ ?.getBody()
+ ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement)
+ ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
+ .find((openingElement) => openingElement.getTagNameNode().getText() === "body")
+ ?.getParentIfKind(SyntaxKind.JsxElement);
+
+ // wrap the body element with the next auth provider
+ bodyElement?.replaceWithText(
+ `
+ ${bodyElement.getText()}
+ `,
+ );
+
+ rootLayoutSource.formatText();
+ rootLayoutSource.saveSync();
+}
+
+function addToSafeActionClient(sourceFile?: SourceFile) {
+ if (!sourceFile) {
+ console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?"));
+ return;
+ }
+
+ sourceFile.addImportDeclaration({
+ namedImports: [{ name: "auth" }],
+ moduleSpecifier: "@/server/auth",
+ });
+
+ // add to end of file
+ sourceFile.addStatements((writer) =>
+ writer.writeLine(`export const authedActionClient = actionClient.use(
+ async ({ next, ctx }) => {
+ const session = await auth();
+ if (!session) {
+ throw new Error("Unauthorized");
+ }
+ return next({ ctx: { ...ctx, session } });
+ }
+);
+`),
+ );
+}
+
+async function checkForNextAuthLayouts(projectDir: string) {
+ const existingLayouts = getExistingSchemas({
+ projectDir,
+ dataSourceName: "filemaker",
+ });
+ const nextAuthLayouts = ["nextauth_user", "nextauth_account", "nextauth_session", "nextauth_verificationToken"];
+
+ const allNextAuthLayoutsExist = nextAuthLayouts.every((layout) =>
+ existingLayouts.some((l) => l.schemaName === layout),
+ );
+
+ if (allNextAuthLayoutsExist) {
+ return;
+ }
+
+ const spinner = await _runExecCommand({
+ command: [`next-auth-adapter-filemaker@${dependencyVersionMap["next-auth-adapter-filemaker"]}`, "install-addon"],
+ projectDir,
+ });
+
+ // If the spinner was used to show the progress, use succeed method on it
+ // If not, use the succeed on a new spinner
+ (spinner ?? ora()).succeed(chalk.green("Successfully installed next-auth addon for FileMaker"));
+
+ console.log("");
+ console.log(chalk.bgYellow(" ACTION REQUIRED: "));
+ console.log(
+ `${chalk.yellowBright("You must now install the NextAuth addon in your FileMaker file.")}
+Learn more: https://proofkit.dev/auth/next-auth\n`,
+ );
+}
diff --git a/packages/cli-old/src/installers/proofkit-auth.ts b/packages/cli-old/src/installers/proofkit-auth.ts
new file mode 100644
index 00000000..89b80532
--- /dev/null
+++ b/packages/cli-old/src/installers/proofkit-auth.ts
@@ -0,0 +1,220 @@
+import path from "node:path";
+import type { OttoAPIKey } from "@proofkit/fmdapi";
+import chalk from "chalk";
+import dotenv from "dotenv";
+import fs from "fs-extra";
+import ora, { type Ora } from "ora";
+import { type SourceFile, SyntaxKind } from "ts-morph";
+import { getLayouts } from "~/cli/fmdapi.js";
+import * as p from "~/cli/prompts.js";
+import { abortIfCancel, UserAbortedError } from "~/cli/utils.js";
+import { PKG_ROOT } from "~/consts.js";
+import { addConfig, runCodegenCommand } from "~/generators/fmdapi.js";
+import { injectTanstackQuery } from "~/generators/tanstack-query.js";
+import { state } from "~/state.js";
+import { addPackageDependency } from "~/utils/addPackageDependency.js";
+import { getSettings } from "~/utils/parseSettings.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+import { addToHeaderSlot } from "./auth-shared.js";
+import { installFmAddon } from "./install-fm-addon.js";
+import { installReactEmail } from "./react-email.js";
+
+export const proofkitAuthInstaller = async () => {
+ const spinner = ora("Installing files for auth...").start();
+
+ const projectDir = state.projectDir;
+ addPackageDependency({
+ projectDir,
+ dependencies: ["@node-rs/argon2", "@oslojs/binary", "@oslojs/crypto", "@oslojs/encoding", "js-cookie"],
+ devMode: false,
+ });
+
+ addPackageDependency({
+ projectDir,
+ dependencies: ["@types/js-cookie"],
+ devMode: true,
+ });
+
+ // copy all files from template/extras/fmaddon-auth to projectDir/src
+ await fs.copy(path.join(PKG_ROOT, "template/extras/fmaddon-auth"), path.join(projectDir, "src"));
+
+ const project = getNewProject(projectDir);
+
+ // ensure tanstack query is installed
+ await injectTanstackQuery({ project });
+
+ // inject signin/signout components to header slots
+ addToHeaderSlot(
+ project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")),
+ "@/components/auth/user-menu",
+ );
+ // addToHeaderSlot(
+ // project.addSourceFileAtPath(
+ // path.join(
+ // projectDir,
+ // "src/components/AppShell/slot-header-mobile-content.tsx"
+ // )
+ // ),
+ // "@/components/clerk-auth/user-menu-mobile"
+ // );
+
+ addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts")));
+
+ await addConfig({
+ config: {
+ type: "fmdapi",
+ envNames: undefined,
+ clientSuffix: "Layout",
+ layouts: [
+ {
+ layoutName: "proofkit_auth_sessions",
+ schemaName: "sessions",
+ strictNumbers: true,
+ },
+ {
+ layoutName: "proofkit_auth_users",
+ schemaName: "users",
+ strictNumbers: true,
+ },
+ {
+ layoutName: "proofkit_auth_email_verification",
+ schemaName: "emailVerification",
+ strictNumbers: true,
+ },
+ {
+ layoutName: "proofkit_auth_password_reset",
+ schemaName: "passwordReset",
+ strictNumbers: true,
+ },
+ ],
+ clearOldFiles: true,
+ validator: false,
+ path: "./src/server/auth/db",
+ },
+ projectDir,
+ runCodegen: false,
+ });
+
+ // install email files based on the email provider in state
+ await installReactEmail({ project, installServerFiles: true });
+
+ protectMainLayout(project.addSourceFileAtPath(path.join(projectDir, "src/app/(main)/layout.tsx")));
+
+ await formatAndSaveSourceFiles(project);
+
+ let hasProofKitLayouts = false;
+ while (!hasProofKitLayouts) {
+ hasProofKitLayouts = await checkForProofKitLayouts(projectDir, spinner);
+
+ if (hasProofKitLayouts) {
+ spinner.text = "Successfully detected all required layouts in your FileMaker file.";
+ } else {
+ const shouldContinue = abortIfCancel(
+ await p.confirm({
+ message: "I have followed the above instructions, continue installing",
+ initialValue: true,
+ active: "Continue",
+ inactive: "Abort",
+ }),
+ );
+
+ if (!shouldContinue) {
+ throw new UserAbortedError();
+ }
+ }
+ }
+ await runCodegenCommand();
+
+ spinner.succeed("Auth installed successfully");
+};
+
+function addToSafeActionClient(sourceFile?: SourceFile) {
+ if (!sourceFile) {
+ console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?"));
+ return;
+ }
+
+ sourceFile.addImportDeclaration({
+ namedImports: [{ name: "getCurrentSession" }],
+ moduleSpecifier: "./auth/utils/session",
+ });
+
+ // add to end of file
+ sourceFile.addStatements((writer) =>
+ writer.writeLine(`export const authedActionClient = actionClient.use(async ({ next, ctx }) => {
+ const { session, user } = await getCurrentSession();
+ if (session === null) {
+ throw new Error("Unauthorized");
+ }
+
+ return next({ ctx: { ...ctx, session, user } });
+});
+`),
+ );
+}
+
+function protectMainLayout(sourceFile: SourceFile) {
+ sourceFile.addImportDeclaration({
+ defaultImport: "Protect",
+ moduleSpecifier: "@/components/auth/protect",
+ });
+
+ // inject query provider into the root layout
+
+ const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport());
+ const bodyElement = exportDefault
+ ?.getBody()
+ ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement)
+ ?.getFirstDescendantByKind(SyntaxKind.JsxElement);
+
+ bodyElement?.replaceWithText(
+ `
+ ${bodyElement?.getText()}
+ `,
+ );
+}
+
+async function checkForProofKitLayouts(projectDir: string, spinner: Ora): Promise {
+ const settings = getSettings();
+
+ const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === "filemaker");
+
+ if (!dataSource) {
+ return false;
+ }
+ if (settings.envFile) {
+ dotenv.config({
+ path: path.join(projectDir, settings.envFile),
+ });
+ }
+ const dataApiKey = process.env[dataSource.envNames.apiKey];
+ const fmFile = process.env[dataSource.envNames.database];
+ const server = process.env[dataSource.envNames.server];
+
+ if (!(dataApiKey && fmFile && server)) {
+ return false;
+ }
+
+ const existingLayouts = await getLayouts({
+ dataApiKey: dataApiKey as OttoAPIKey,
+ fmFile,
+ server,
+ });
+ const proofkitAuthLayouts = [
+ "proofkit_auth_sessions",
+ "proofkit_auth_users",
+ "proofkit_auth_email_verification",
+ "proofkit_auth_password_reset",
+ ];
+
+ const allProofkitAuthLayoutsExist = proofkitAuthLayouts.every((layout) => existingLayouts.some((l) => l === layout));
+
+ if (allProofkitAuthLayoutsExist) {
+ return true;
+ }
+
+ spinner.warn("Required layouts not found");
+ await installFmAddon({ addonName: "auth" });
+
+ return false;
+}
diff --git a/packages/cli-old/src/installers/proofkit-webviewer.ts b/packages/cli-old/src/installers/proofkit-webviewer.ts
new file mode 100644
index 00000000..f3a1a23f
--- /dev/null
+++ b/packages/cli-old/src/installers/proofkit-webviewer.ts
@@ -0,0 +1,84 @@
+import path from "node:path";
+import type { OttoAPIKey } from "@proofkit/fmdapi";
+import chalk from "chalk";
+import dotenv from "dotenv";
+import { getLayouts } from "~/cli/fmdapi.js";
+import * as p from "~/cli/prompts.js";
+import { abortIfCancel, UserAbortedError } from "~/cli/utils.js";
+import { state } from "~/state.js";
+import { getSettings } from "~/utils/parseSettings.js";
+import { installFmAddon } from "./install-fm-addon.js";
+
+export async function checkForWebViewerLayouts(): Promise {
+ const settings = getSettings();
+
+ const dataSource = settings.dataSources
+ .filter((s: { type: string }) => s.type === "fm")
+ .find((s: { name: string; type: string }) => s.name === "filemaker") as
+ | {
+ type: "fm";
+ name: string;
+ envNames: { database: string; server: string; apiKey: string };
+ }
+ | undefined;
+
+ if (!dataSource) {
+ return false;
+ }
+ if (settings.envFile) {
+ dotenv.config({
+ path: path.join(state.projectDir, settings.envFile),
+ });
+ }
+ const dataApiKey = process.env[dataSource.envNames.apiKey] as OttoAPIKey | undefined;
+ const fmFile = process.env[dataSource.envNames.database];
+ const server = process.env[dataSource.envNames.server];
+
+ if (!(dataApiKey && fmFile && server)) {
+ return false;
+ }
+
+ const existingLayouts = await getLayouts({
+ dataApiKey,
+ fmFile,
+ server,
+ });
+ const webviewerLayouts = ["ProofKitWV"];
+
+ const allWebViewerLayoutsExist = webviewerLayouts.every((layout) =>
+ existingLayouts.some((l: string) => l === layout),
+ );
+
+ if (allWebViewerLayoutsExist) {
+ console.log(
+ chalk.green("Successfully detected all required layouts for ProofKit WebViewer in your FileMaker file."),
+ );
+ return true;
+ }
+
+ await installFmAddon({ addonName: "wv" });
+
+ return false;
+}
+
+export async function ensureWebViewerAddonInstalled() {
+ let hasWebViewerLayouts = false;
+ while (!hasWebViewerLayouts) {
+ hasWebViewerLayouts = await checkForWebViewerLayouts();
+
+ if (!hasWebViewerLayouts) {
+ const shouldContinue = abortIfCancel(
+ await p.confirm({
+ message: "I have followed the above instructions, continue installing",
+ initialValue: true,
+ active: "Continue",
+ inactive: "Abort",
+ }),
+ );
+
+ if (!shouldContinue) {
+ throw new UserAbortedError();
+ }
+ }
+ }
+}
diff --git a/packages/cli-old/src/installers/react-email.ts b/packages/cli-old/src/installers/react-email.ts
new file mode 100644
index 00000000..59a90749
--- /dev/null
+++ b/packages/cli-old/src/installers/react-email.ts
@@ -0,0 +1,211 @@
+import path from "node:path";
+import chalk from "chalk";
+import fs from "fs-extra";
+import type { Project } from "ts-morph";
+import type { PackageJson } from "type-fest";
+import * as p from "~/cli/prompts.js";
+
+import { abortIfCancel } from "~/cli/utils.js";
+import { PKG_ROOT } from "~/consts.js";
+import { installDependencies } from "~/helpers/installDependencies.js";
+import { isNonInteractiveMode, state } from "~/state.js";
+import { addPackageDependency } from "~/utils/addPackageDependency.js";
+import { addToEnv } from "~/utils/addToEnvs.js";
+import { logger } from "~/utils/logger.js";
+import { getSettings, setSettings } from "~/utils/parseSettings.js";
+import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
+
+export async function installReactEmail({
+ ...args
+}: {
+ project?: Project;
+ noInstall?: boolean;
+ installServerFiles?: boolean;
+}) {
+ const projectDir = state.projectDir;
+
+ // Exit early if already installed
+ const settings = getSettings();
+ if (settings.ui === "shadcn") {
+ return false;
+ }
+ if (settings.reactEmail) {
+ return false;
+ }
+
+ // Ensure emails directory exists
+ fs.ensureDirSync(path.join(projectDir, "src/emails"));
+ addPackageDependency({
+ dependencies: ["@react-email/components", "@react-email/render"],
+ devMode: false,
+ projectDir,
+ });
+ addPackageDependency({
+ dependencies: ["react-email", "@react-email/preview-server"],
+ devMode: true,
+ projectDir,
+ });
+
+ // add a script to package.json
+ const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as PackageJson;
+ if (!pkgJson.scripts) {
+ pkgJson.scripts = {};
+ }
+ pkgJson.scripts["email:preview"] = "email dev --port 3010 --dir=src/emails";
+ fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, {
+ spaces: 2,
+ });
+
+ const project = args.project ?? getNewProject(projectDir);
+
+ if (args.installServerFiles) {
+ const emailProvider = state.emailProvider;
+ if (emailProvider === "plunk") {
+ await installPlunk({ project });
+ } else if (emailProvider === "resend") {
+ await installResend({ project });
+ } else {
+ await fs.copy(
+ path.join(PKG_ROOT, "template/extras/emailProviders/none/email.tsx"),
+ path.join(projectDir, "src/server/auth/email.tsx"),
+ );
+ }
+ }
+
+ // Copy base email template(s) into src/emails for preview and reuse
+ await fs.copy(
+ path.join(PKG_ROOT, "template/extras/emailTemplates/generic.tsx"),
+ path.join(projectDir, "src/emails/generic.tsx"),
+ );
+ if (args.installServerFiles) {
+ await fs.copy(
+ path.join(PKG_ROOT, "template/extras/emailTemplates/auth-code.tsx"),
+ path.join(projectDir, "src/emails/auth-code.tsx"),
+ );
+ }
+
+ if (!args.project) {
+ await formatAndSaveSourceFiles(project);
+ }
+
+ // Mark as installed
+ setSettings({
+ ...settings,
+ reactEmail: true,
+ reactEmailServer: Boolean(args.installServerFiles) || settings.reactEmailServer,
+ });
+
+ // Install dependencies unless explicitly skipped
+ if (!args.noInstall) {
+ await installDependencies({ projectDir });
+ }
+ return true;
+}
+
+export async function installPlunk({ project }: { project?: Project }) {
+ const projectDir = state.projectDir;
+ addPackageDependency({
+ dependencies: ["@plunk/node"],
+ devMode: false,
+ projectDir,
+ });
+
+ let apiKey: string;
+ if (typeof state.apiKey === "string") {
+ apiKey = state.apiKey;
+ } else if (isNonInteractiveMode()) {
+ apiKey = "";
+ } else {
+ apiKey = abortIfCancel(
+ await p.text({
+ message: `Enter your Plunk API key\n${chalk.dim(
+ "Enter your Secret API Key from https://app.useplunk.com/settings/api",
+ )}`,
+ placeholder: "...or leave blank to do this later",
+ }),
+ );
+ }
+
+ if (!apiKey) {
+ logger.warn("You will need to add your Plunk API key to the .env file manually for your app to run.");
+ }
+
+ console.log("");
+
+ await addToEnv({
+ projectDir,
+ project,
+ envs: [
+ {
+ name: "PLUNK_API_KEY",
+ zodValue: `z.string().startsWith("sk_")`,
+ type: "server",
+ defaultValue: apiKey,
+ },
+ ],
+ });
+
+ await fs.copy(
+ path.join(PKG_ROOT, "template/extras/emailProviders/plunk/service.ts"),
+ path.join(projectDir, "src/server/services/plunk.ts"),
+ );
+
+ await fs.copy(
+ path.join(PKG_ROOT, "template/extras/emailProviders/plunk/email.tsx"),
+ path.join(projectDir, "src/server/auth/email.tsx"),
+ );
+}
+
+export async function installResend({ project }: { project?: Project }) {
+ const projectDir = state.projectDir;
+ addPackageDependency({
+ dependencies: ["resend"],
+ devMode: false,
+ projectDir,
+ });
+
+ let apiKey: string;
+ if (typeof state.apiKey === "string") {
+ apiKey = state.apiKey;
+ } else if (isNonInteractiveMode()) {
+ apiKey = "";
+ } else {
+ apiKey = abortIfCancel(
+ await p.text({
+ message: `Enter your Resend API key\n${chalk.dim(
+ `Only "Sending Access" permission required: https://resend.com/api-keys`,
+ )}`,
+ placeholder: "...or leave blank to do this later",
+ }),
+ );
+ }
+
+ if (!apiKey) {
+ logger.warn("You will need to add your Resend API key to the .env file manually for your app to run.");
+ }
+
+ console.log("");
+
+ await addToEnv({
+ projectDir,
+ project,
+ envs: [
+ {
+ name: "RESEND_API_KEY",
+ zodValue: `z.string().startsWith("re_")`,
+ type: "server",
+ defaultValue: apiKey,
+ },
+ ],
+ });
+
+ await fs.copy(
+ path.join(PKG_ROOT, "template/extras/emailProviders/resend/service.ts"),
+ path.join(projectDir, "src/server/services/resend.ts"),
+ );
+
+ await fs.copy(
+ path.join(PKG_ROOT, "template/extras/emailProviders/resend/email.tsx"),
+ path.join(projectDir, "src/server/auth/email.tsx"),
+ );
+}
diff --git a/packages/cli-old/src/state.ts b/packages/cli-old/src/state.ts
new file mode 100644
index 00000000..58711d4b
--- /dev/null
+++ b/packages/cli-old/src/state.ts
@@ -0,0 +1,33 @@
+import { z } from "zod/v4";
+
+const schema = z
+ .object({
+ ci: z.boolean().default(false),
+ nonInteractive: z.boolean().default(false),
+ debug: z.boolean().default(false),
+ localBuild: z.boolean().default(false),
+ baseCommand: z.enum(["add", "init", "deploy", "upgrade", "remove"]).optional().catch(undefined),
+ appType: z.enum(["browser", "webviewer"]).optional().catch(undefined),
+ ui: z.enum(["shadcn", "mantine"]).optional().catch("mantine"),
+ projectDir: z.string().default(process.cwd()),
+ authType: z.enum(["clerk", "fmaddon"]).optional(),
+ emailProvider: z.enum(["plunk", "resend", "none"]).optional(),
+ dataSource: z.enum(["filemaker", "none"]).optional(),
+ })
+ .passthrough();
+
+type ProgramState = z.infer;
+export let state: ProgramState = schema.parse({});
+
+export function initProgramState(args: unknown) {
+ const parsed = schema.safeParse(args);
+ if (parsed.success) {
+ const mergedState = { ...state, ...parsed.data };
+ const nonInteractive = mergedState.nonInteractive || mergedState.ci;
+ state = { ...mergedState, ci: nonInteractive, nonInteractive };
+ }
+}
+
+export function isNonInteractiveMode() {
+ return state.nonInteractive || state.ci;
+}
diff --git a/packages/cli-old/src/upgrades/cursorRules.ts b/packages/cli-old/src/upgrades/cursorRules.ts
new file mode 100644
index 00000000..1338225f
--- /dev/null
+++ b/packages/cli-old/src/upgrades/cursorRules.ts
@@ -0,0 +1,41 @@
+import path from "node:path";
+import fs from "fs-extra";
+
+import { PKG_ROOT } from "~/consts.js";
+import { state } from "~/state.js";
+import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
+
+export async function copyCursorRules() {
+ const projectDir = state.projectDir;
+ const extrasDir = path.join(PKG_ROOT, "template/extras");
+ const cursorRulesSrcDir = path.join(extrasDir, "_cursor/rules");
+ const cursorRulesDestDir = path.join(projectDir, ".cursor/rules");
+
+ if (!fs.existsSync(cursorRulesSrcDir)) {
+ return;
+ }
+
+ const pkgManager = getUserPkgManager();
+ await fs.ensureDir(cursorRulesDestDir);
+ await fs.copy(cursorRulesSrcDir, cursorRulesDestDir);
+
+ // Copy package manager specific rules
+ const conditionalRulesDir = path.join(extrasDir, "_cursor/conditional-rules");
+
+ const packageManagerRules = {
+ pnpm: "pnpm.mdc",
+ npm: "npm.mdc",
+ yarn: "yarn.mdc",
+ };
+
+ const selectedRule = packageManagerRules[pkgManager as keyof typeof packageManagerRules];
+
+ if (selectedRule) {
+ const ruleSrc = path.join(conditionalRulesDir, selectedRule);
+ const ruleDest = path.join(cursorRulesDestDir, "package-manager.mdc");
+
+ if (fs.existsSync(ruleSrc)) {
+ await fs.copy(ruleSrc, ruleDest, { overwrite: true });
+ }
+ }
+}
diff --git a/packages/cli-old/src/upgrades/index.ts b/packages/cli-old/src/upgrades/index.ts
new file mode 100644
index 00000000..b72dbc98
--- /dev/null
+++ b/packages/cli-old/src/upgrades/index.ts
@@ -0,0 +1,69 @@
+import { type appTypes, getSettings, mergeSettings } from "~/utils/parseSettings.js";
+import { copyCursorRules } from "./cursorRules.js";
+import { addShadcn } from "./shadcn.js";
+
+interface Upgrade {
+ key: string;
+ title: string;
+ description: string;
+ appType: (typeof appTypes)[number][];
+ function: () => Promise;
+}
+
+const availableUpgrades: Upgrade[] = [
+ {
+ key: "cursorRules",
+ title: "Upgrade Cursor Rules",
+ description: "Upgrade the .cursor rules in your project to the latest version.",
+ appType: ["browser"],
+ function: copyCursorRules,
+ },
+ {
+ key: "shadcn",
+ title: "Add Shadcn",
+ description:
+ "Add Shadcn to your project, to support easily adding new components from a variety of component registries.",
+ appType: ["browser", "webviewer"],
+ function: addShadcn,
+ },
+];
+
+export type UpgradeKeys = (typeof availableUpgrades)[number]["key"];
+
+export function checkForAvailableUpgrades() {
+ const settings = getSettings();
+ if (settings.ui === "shadcn") {
+ return [];
+ }
+
+ const appliedUpgrades = settings.appliedUpgrades;
+
+ const neededUpgrades = availableUpgrades.filter(
+ (upgrade) => !appliedUpgrades.includes(upgrade.key) && upgrade.appType.includes(settings.appType),
+ );
+
+ return neededUpgrades.map(({ key, title, description }) => ({
+ key,
+ title,
+ description,
+ }));
+}
+
+export async function runAllAvailableUpgrades() {
+ const upgrades = checkForAvailableUpgrades();
+ const settings = getSettings();
+ if (settings.ui === "shadcn") {
+ return;
+ }
+
+ for (const upgrade of upgrades) {
+ const upgradeFunction = availableUpgrades.find((u) => u.key === upgrade.key)?.function;
+ if (upgradeFunction) {
+ await upgradeFunction();
+ const appliedUpgrades = settings.appliedUpgrades;
+ mergeSettings({
+ appliedUpgrades: [...appliedUpgrades, upgrade.key],
+ });
+ }
+ }
+}
diff --git a/packages/cli-old/src/upgrades/shadcn.ts b/packages/cli-old/src/upgrades/shadcn.ts
new file mode 100644
index 00000000..d385a4d0
--- /dev/null
+++ b/packages/cli-old/src/upgrades/shadcn.ts
@@ -0,0 +1,53 @@
+import path from "node:path";
+import fs from "fs-extra";
+
+import { PKG_ROOT } from "~/consts.js";
+import { installDependencies } from "~/helpers/installDependencies.js";
+import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js";
+import { state } from "~/state.js";
+import { addPackageDependency } from "~/utils/addPackageDependency.js";
+
+const BASE_DEPS = [
+ "@radix-ui/react-slot",
+ "@tailwindcss/postcss",
+ "class-variance-authority",
+ "clsx",
+ "lucide-react",
+ "tailwind-merge",
+ "tailwindcss",
+ "tw-animate-css",
+] as AvailableDependencies[];
+const BASE_DEV_DEPS = [] as AvailableDependencies[];
+
+export async function addShadcn() {
+ const projectDir = state.projectDir;
+
+ const TEMPLATE_ROOT = path.join(PKG_ROOT, "template/nextjs");
+
+ // 1. Add dependencies
+ addPackageDependency({
+ dependencies: BASE_DEPS,
+ devMode: false,
+ projectDir,
+ });
+ addPackageDependency({
+ dependencies: BASE_DEV_DEPS,
+ devMode: true,
+ projectDir,
+ });
+
+ // 2. Copy config and utility files
+ fs.copySync(path.join(TEMPLATE_ROOT, "components.json"), path.join(projectDir, "components.json"));
+ fs.copySync(path.join(TEMPLATE_ROOT, "postcss.config.cjs"), path.join(projectDir, "postcss.config.cjs"));
+ fs.copySync(path.join(TEMPLATE_ROOT, "src/utils/styles.ts"), path.join(projectDir, "src/utils/styles.ts"));
+ fs.copySync(
+ path.join(TEMPLATE_ROOT, "src/config/theme/globals.css"),
+ path.join(projectDir, "src/config/theme/globals.css"),
+ );
+
+ // 3. Install dependencies
+ await installDependencies();
+
+ // 4. Success message
+ console.log("\n✅ shadcn/ui + Tailwind v4 upgrade complete!\n");
+}
diff --git a/packages/cli-old/src/utils/addPackageDependency.ts b/packages/cli-old/src/utils/addPackageDependency.ts
new file mode 100644
index 00000000..c2d139e7
--- /dev/null
+++ b/packages/cli-old/src/utils/addPackageDependency.ts
@@ -0,0 +1,32 @@
+import path from "node:path";
+import fs from "fs-extra";
+import sortPackageJson from "sort-package-json";
+import type { PackageJson } from "type-fest";
+
+import { type AvailableDependencies, dependencyVersionMap } from "~/installers/dependencyVersionMap.js";
+import { state } from "~/state.js";
+
+export const addPackageDependency = (opts: {
+ dependencies: AvailableDependencies[];
+ devMode: boolean;
+ projectDir?: string;
+}) => {
+ const { dependencies, devMode, projectDir = state.projectDir } = opts;
+
+ const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as PackageJson;
+
+ for (const pkgName of dependencies) {
+ const version = dependencyVersionMap[pkgName];
+
+ if (devMode && pkgJson.devDependencies) {
+ pkgJson.devDependencies[pkgName] = version;
+ } else if (pkgJson.dependencies) {
+ pkgJson.dependencies[pkgName] = version;
+ }
+ }
+ const sortedPkgJson = sortPackageJson(pkgJson);
+
+ fs.writeJSONSync(path.join(projectDir, "package.json"), sortedPkgJson, {
+ spaces: 2,
+ });
+};
diff --git a/packages/cli-old/src/utils/addToEnvs.ts b/packages/cli-old/src/utils/addToEnvs.ts
new file mode 100644
index 00000000..5af7e131
--- /dev/null
+++ b/packages/cli-old/src/utils/addToEnvs.ts
@@ -0,0 +1,131 @@
+import { execSync } from "node:child_process";
+import path from "node:path";
+import fs from "fs-extra";
+import { type Project, SyntaxKind } from "ts-morph";
+
+import { findT3EnvFile } from "~/installers/envVars.js";
+import { state } from "~/state.js";
+import { formatAndSaveSourceFiles, getNewProject } from "./ts-morph.js";
+
+interface EnvSchema {
+ name: string;
+ zodValue: string;
+ /** This value will be added to the .env file, unless `addToRuntimeEnv` is set to `false`. */
+ defaultValue?: string;
+ type: "server" | "client";
+ addToRuntimeEnv?: boolean;
+}
+
+export async function addToEnv({
+ projectDir = state.projectDir,
+ envs,
+ envFileDescription,
+ ...args
+}: {
+ projectDir?: string;
+ project?: Project;
+ envs: EnvSchema[];
+ envFileDescription?: string;
+}) {
+ const envSchemaFile = findT3EnvFile();
+
+ const project = args.project ?? getNewProject(projectDir);
+ const schemaFile = project.addSourceFileAtPath(envSchemaFile);
+
+ if (!schemaFile) {
+ throw new Error("Schema file not found");
+ }
+
+ // Find the createEnv call expression
+ const createEnvCall = schemaFile
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
+ .find((callExpr) => callExpr.getExpression().getText() === "createEnv");
+
+ if (!createEnvCall) {
+ throw new Error(
+ "Could not find createEnv call in schema file. Make sure you have a valid env.ts file with createEnv setup.",
+ );
+ }
+
+ // Get the server object property
+ const opts = createEnvCall.getArguments()[0];
+ if (!opts) {
+ throw new Error("createEnv call is missing options argument");
+ }
+
+ const serverProperty = opts
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
+ .find((prop) => prop.getName() === "server")
+ ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
+
+ const clientProperty = opts
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
+ .find((prop) => prop.getName() === "client")
+ ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
+
+ const runtimeEnvProperty = opts
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
+ .find((prop) => prop.getName() === "experimental__runtimeEnv")
+ ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
+
+ const serverEnvs = envs.filter((env) => env.type === "server");
+ const clientEnvs = envs.filter((env) => env.type === "client");
+
+ for (const env of serverEnvs) {
+ serverProperty?.addPropertyAssignment({
+ name: env.name,
+ initializer: env.zodValue,
+ });
+ }
+
+ for (const env of clientEnvs) {
+ clientProperty?.addPropertyAssignment({
+ name: env.name,
+ initializer: env.zodValue,
+ });
+
+ runtimeEnvProperty?.addPropertyAssignment({
+ name: env.name,
+ initializer: `process.env.${env.name}`,
+ });
+ }
+
+ const envsString = envs
+ .filter((env) => env.addToRuntimeEnv ?? true)
+ .map((env) => `${env.name}=${env.defaultValue ?? ""}`)
+ .join("\n");
+
+ const dotEnvFile = path.join(projectDir, ".env");
+
+ // Only handle .env file if it already exists
+ if (fs.existsSync(dotEnvFile)) {
+ const currentFile = fs.readFileSync(dotEnvFile, "utf-8");
+
+ // Ensure .env is in .gitignore using command line
+ const gitIgnoreFile = path.join(projectDir, ".gitignore");
+ try {
+ let gitIgnoreContent = "";
+ if (fs.existsSync(gitIgnoreFile)) {
+ gitIgnoreContent = fs.readFileSync(gitIgnoreFile, "utf-8");
+ }
+
+ if (!gitIgnoreContent.includes(".env")) {
+ execSync(`echo ".env" >> "${gitIgnoreFile}"`, { cwd: projectDir });
+ }
+ } catch (_error) {
+ // Silently ignore gitignore errors
+ }
+
+ const newContent = `${currentFile}
+${envFileDescription ? `# ${envFileDescription}\n${envsString}` : envsString}
+ `;
+
+ fs.writeFileSync(dotEnvFile, newContent);
+ }
+
+ if (!args.project) {
+ await formatAndSaveSourceFiles(project);
+ }
+
+ return schemaFile;
+}
diff --git a/packages/cli-old/src/utils/formatting.ts b/packages/cli-old/src/utils/formatting.ts
new file mode 100644
index 00000000..8522e45a
--- /dev/null
+++ b/packages/cli-old/src/utils/formatting.ts
@@ -0,0 +1,24 @@
+import { execa } from "execa";
+import type { Project } from "ts-morph";
+
+import { state } from "~/state.js";
+
+/**
+ * Formats all source files in a ts-morph Project using biome and saves the changes.
+ * @param project The ts-morph Project containing the files to format
+ */
+export async function formatAndSaveSourceFiles(project: Project) {
+ await project.save(); // save files first
+ try {
+ // Run biome format on the project directory
+ await execa("npx", ["@biomejs/biome", "format", "--write", state.projectDir], {
+ cwd: state.projectDir,
+ });
+ } catch (error) {
+ if (state.debug) {
+ console.log("Error formatting files with biome");
+ console.error(error);
+ }
+ // Continue even if formatting fails
+ }
+}
diff --git a/packages/cli-old/src/utils/getProofKitVersion.ts b/packages/cli-old/src/utils/getProofKitVersion.ts
new file mode 100644
index 00000000..496e46a2
--- /dev/null
+++ b/packages/cli-old/src/utils/getProofKitVersion.ts
@@ -0,0 +1,38 @@
+import path from "node:path";
+import fs from "fs-extra";
+import type { PackageJson } from "type-fest";
+
+import { PKG_ROOT } from "~/consts.js";
+
+export const getVersion = () => {
+ const packageJsonPath = path.join(PKG_ROOT, "package.json");
+
+ const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson;
+
+ return packageJsonContent.version ?? "1.0.0";
+};
+
+export const getFmdapiVersion = () => {
+ return __FMDAPI_VERSION__;
+};
+
+export const getNodeMajorVersion = () => {
+ const defaultVersion = "22";
+ try {
+ return process.versions.node.split(".")[0] ?? defaultVersion;
+ } catch {
+ return defaultVersion;
+ }
+};
+
+export const getProofkitBetterAuthVersion = () => {
+ return __BETTER_AUTH_VERSION__;
+};
+
+export const getProofkitWebviewerVersion = () => {
+ return __WEBVIEWER_VERSION__;
+};
+
+export const getTypegenVersion = () => {
+ return __TYPEGEN_VERSION__;
+};
diff --git a/packages/cli-old/src/utils/getUserPkgManager.ts b/packages/cli-old/src/utils/getUserPkgManager.ts
new file mode 100644
index 00000000..d2e3afdc
--- /dev/null
+++ b/packages/cli-old/src/utils/getUserPkgManager.ts
@@ -0,0 +1,21 @@
+export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
+
+export const getUserPkgManager: () => PackageManager = () => {
+ // This environment variable is set by npm and yarn but pnpm seems less consistent
+ const userAgent = process.env.npm_config_user_agent;
+
+ if (userAgent) {
+ if (userAgent.startsWith("yarn")) {
+ return "yarn";
+ }
+ if (userAgent.startsWith("pnpm")) {
+ return "pnpm";
+ }
+ if (userAgent.startsWith("bun")) {
+ return "bun";
+ }
+ return "npm";
+ }
+ // If no user agent is set, assume pnpm
+ return "pnpm";
+};
diff --git a/packages/cli-old/src/utils/isTTYError.ts b/packages/cli-old/src/utils/isTTYError.ts
new file mode 100644
index 00000000..ccf602ed
--- /dev/null
+++ b/packages/cli-old/src/utils/isTTYError.ts
@@ -0,0 +1 @@
+export class IsTTYError extends Error {}
diff --git a/packages/cli-old/src/utils/logger.ts b/packages/cli-old/src/utils/logger.ts
new file mode 100644
index 00000000..3ddb9775
--- /dev/null
+++ b/packages/cli-old/src/utils/logger.ts
@@ -0,0 +1,19 @@
+import chalk from "chalk";
+
+export const logger = {
+ error(...args: unknown[]) {
+ console.log(chalk.red(...args));
+ },
+ warn(...args: unknown[]) {
+ console.log(chalk.yellow(...args));
+ },
+ info(...args: unknown[]) {
+ console.log(chalk.cyan(...args));
+ },
+ success(...args: unknown[]) {
+ console.log(chalk.green(...args));
+ },
+ dim(...args: unknown[]) {
+ console.log(chalk.dim(...args));
+ },
+};
diff --git a/packages/cli-old/src/utils/parseNameAndPath.ts b/packages/cli-old/src/utils/parseNameAndPath.ts
new file mode 100644
index 00000000..a4d4507e
--- /dev/null
+++ b/packages/cli-old/src/utils/parseNameAndPath.ts
@@ -0,0 +1,42 @@
+import pathModule from "node:path";
+
+import { removeTrailingSlash } from "./removeTrailingSlash.js";
+
+/**
+ * Parses the appName and its path from the user input.
+ *
+ * Returns a tuple of of `[appName, path]`, where `appName` is the name put in the "package.json"
+ * file and `path` is the path to the directory where the app will be created.
+ *
+ * If `appName` is ".", the name of the directory will be used instead. Handles the case where the
+ * input includes a scoped package name in which case that is being parsed as the name, but not
+ * included as the path.
+ *
+ * For example:
+ *
+ * - dir/@mono/app => ["@mono/app", "dir/app"]
+ * - dir/app => ["app", "dir/app"]
+ */
+export const parseNameAndPath = (rawInput: string) => {
+ const input = removeTrailingSlash(rawInput);
+
+ const paths = input.split("/");
+
+ let appName = paths.at(-1) ?? "";
+
+ // If the user ran `npx proofkit .` or similar, the appName should be the current directory
+ if (appName === ".") {
+ const parsedCwd = pathModule.resolve(process.cwd());
+ appName = pathModule.basename(parsedCwd);
+ }
+
+ // If the first part is a @, it's a scoped package
+ const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@"));
+ if (paths.findIndex((p) => p.startsWith("@")) !== -1) {
+ appName = paths.slice(indexOfDelimiter).join("/");
+ }
+
+ const path = paths.filter((p) => !p.startsWith("@")).join("/");
+
+ return [appName, path] as const;
+};
diff --git a/packages/cli-old/src/utils/parseSettings.ts b/packages/cli-old/src/utils/parseSettings.ts
new file mode 100644
index 00000000..eb77a8ec
--- /dev/null
+++ b/packages/cli-old/src/utils/parseSettings.ts
@@ -0,0 +1,153 @@
+import path from "node:path";
+import fs from "fs-extra";
+import { z } from "zod/v4";
+
+import { state } from "~/state.js";
+
+const authSchema = z
+ .discriminatedUnion("type", [
+ z.object({
+ type: z.literal("clerk"),
+ }),
+ z.object({
+ type: z.literal("next-auth"),
+ }),
+ z.object({
+ type: z.literal("proofkit").transform(() => "fmaddon"),
+ }),
+ z.object({
+ type: z.literal("fmaddon"),
+ }),
+ z.object({
+ type: z.literal("better-auth"),
+ }),
+ z.object({
+ type: z.literal("none"),
+ }),
+ ])
+ .default({ type: "none" });
+
+export const envNamesSchema = z.object({
+ database: z.string().default("FM_DATABASE"),
+ server: z.string().default("FM_SERVER"),
+ apiKey: z.string().default("OTTO_API_KEY"),
+});
+export const dataSourceSchema = z.discriminatedUnion("type", [
+ z.object({
+ type: z.literal("fm"),
+ name: z.string(),
+ envNames: envNamesSchema,
+ }),
+ z.object({
+ type: z.literal("supabase"),
+ name: z.string(),
+ }),
+]);
+export type DataSource = z.infer;
+
+export const appTypes = ["browser", "webviewer"] as const;
+
+export const uiTypes = ["shadcn", "mantine"] as const;
+export type Ui = (typeof uiTypes)[number];
+
+const settingsSchema = z.discriminatedUnion("ui", [
+ z.object({
+ ui: z.literal("mantine"),
+ appType: z.enum(appTypes).default("browser"),
+ auth: authSchema,
+ envFile: z.string().optional(),
+ dataSources: z.array(dataSourceSchema).default([]),
+ tanstackQuery: z.boolean().catch(false),
+ replacedMainPage: z.boolean().catch(false),
+ // Whether React Email scaffolding has been installed
+ reactEmail: z.boolean().catch(false),
+ // Whether provider-specific server email sender files have been installed
+ reactEmailServer: z.boolean().catch(false),
+ appliedUpgrades: z.array(z.string()).default([]),
+ registryUrl: z.url().optional(),
+ registryTemplates: z.array(z.string()).default([]),
+ }),
+ z.object({
+ ui: z.literal("shadcn"),
+ appType: z.enum(appTypes).default("browser"),
+ envFile: z.string().optional(),
+ dataSources: z.array(dataSourceSchema).default([]),
+ replacedMainPage: z.boolean().catch(false),
+ registryUrl: z.url().optional(),
+ registryTemplates: z.array(z.string()).default([]),
+ }),
+]);
+
+export const defaultSettings = settingsSchema.parse({
+ auth: { type: "none" },
+ ui: "shadcn",
+ appType: "browser",
+ dataSources: [],
+ replacedMainPage: false,
+ registryTemplates: [],
+});
+
+let settings: Settings | undefined;
+export const getSettings = () => {
+ if (settings) {
+ return settings;
+ }
+
+ const settingsPath = path.join(state.projectDir, "proofkit.json");
+
+ // Check if the settings file exists before trying to read it
+ if (!fs.existsSync(settingsPath)) {
+ throw new Error(`ProofKit settings file not found at: ${settingsPath}`);
+ }
+
+ let settingsFile: unknown = fs.readJSONSync(settingsPath);
+
+ if (typeof settingsFile === "object" && settingsFile !== null && !("ui" in settingsFile)) {
+ settingsFile = { ...settingsFile, ui: "mantine" };
+ }
+
+ const parsed = settingsSchema.parse(settingsFile);
+
+ state.appType = parsed.appType;
+ return parsed;
+};
+
+export type Settings = z.infer;
+
+export function mergeSettings(_settings: Partial) {
+ const settings = getSettings();
+ const merged = { ...settings, ..._settings };
+ const validated = settingsSchema.parse(merged);
+ setSettings(validated);
+}
+
+export function setSettings(_settings: Settings) {
+ fs.writeJSONSync(path.join(state.projectDir, "proofkit.json"), _settings, {
+ spaces: 2,
+ });
+ settings = _settings;
+ return settings;
+}
+
+/**
+ * Validates and sets the envFile in settings only if the file exists.
+ * Used during stealth initialization to avoid setting non-existent env files.
+ */
+export function validateAndSetEnvFile(envFileName = ".env") {
+ const settings = getSettings();
+ const envFilePath = path.join(state.projectDir, envFileName);
+
+ if (fs.existsSync(envFilePath)) {
+ const updatedSettings = { ...settings, envFile: envFileName };
+ setSettings(updatedSettings);
+ return envFileName;
+ }
+
+ // If no env file exists, ensure envFile is undefined in settings
+ if (settings.envFile) {
+ const { envFile, ...settingsWithoutEnvFile } = settings;
+ setSettings(settingsWithoutEnvFile as Settings);
+ }
+
+ return undefined;
+}
diff --git a/packages/cli-old/src/utils/proofkitReleaseChannel.ts b/packages/cli-old/src/utils/proofkitReleaseChannel.ts
new file mode 100644
index 00000000..aa7ecf17
--- /dev/null
+++ b/packages/cli-old/src/utils/proofkitReleaseChannel.ts
@@ -0,0 +1,93 @@
+import path from "node:path";
+import fs from "fs-extra";
+import semver from "semver";
+
+import {
+ getFmdapiVersion,
+ getProofkitBetterAuthVersion,
+ getProofkitWebviewerVersion,
+ getTypegenVersion,
+ getVersion,
+} from "~/utils/getProofKitVersion.js";
+
+export type ProofkitReleaseTag = "latest" | "beta";
+
+interface ChangesetPreState {
+ mode?: string;
+ tag?: string;
+}
+
+function findRepoRootWithChangeset(startDir: string): string | null {
+ let currentDir = path.resolve(startDir);
+ const { root } = path.parse(currentDir);
+
+ while (currentDir !== root) {
+ if (fs.existsSync(path.join(currentDir, ".changeset"))) {
+ return currentDir;
+ }
+ currentDir = path.dirname(currentDir);
+ }
+
+ return null;
+}
+
+function readChangesetPreState(startDir = process.cwd()): ChangesetPreState | null {
+ const repoRoot = findRepoRootWithChangeset(startDir);
+ if (!repoRoot) {
+ return null;
+ }
+
+ const prePath = path.join(repoRoot, ".changeset", "pre.json");
+ if (!fs.existsSync(prePath)) {
+ return null;
+ }
+
+ try {
+ return fs.readJSONSync(prePath) as ChangesetPreState;
+ } catch {
+ return null;
+ }
+}
+
+export function hasAnyPrereleaseVersion(versionCandidates?: Array) {
+ if (versionCandidates) {
+ return versionCandidates.some((version) => {
+ if (!version) {
+ return false;
+ }
+ return semver.valid(version) && semver.prerelease(version);
+ });
+ }
+
+ const readVersion = (getter: () => string) => {
+ try {
+ return getter();
+ } catch {
+ return null;
+ }
+ };
+
+ const proofkitVersions = [
+ readVersion(getVersion),
+ readVersion(getFmdapiVersion),
+ readVersion(getProofkitWebviewerVersion),
+ readVersion(getTypegenVersion),
+ readVersion(getProofkitBetterAuthVersion),
+ ].filter((version): version is string => Boolean(version));
+
+ return proofkitVersions.some((version) => semver.valid(version) && semver.prerelease(version));
+}
+
+export function getProofkitReleaseTag(startDir = process.cwd()): ProofkitReleaseTag {
+ const preState = readChangesetPreState(startDir);
+
+ if (preState?.mode === "pre" && preState.tag === "beta") {
+ return "beta";
+ }
+
+ if (hasAnyPrereleaseVersion()) {
+ return "beta";
+ }
+
+ return "latest";
+}
diff --git a/packages/cli-old/src/utils/removeTrailingSlash.ts b/packages/cli-old/src/utils/removeTrailingSlash.ts
new file mode 100644
index 00000000..051c3322
--- /dev/null
+++ b/packages/cli-old/src/utils/removeTrailingSlash.ts
@@ -0,0 +1,6 @@
+export const removeTrailingSlash = (input: string) => {
+ if (input.length > 1 && input.endsWith("/")) {
+ return input.slice(0, -1);
+ }
+ return input;
+};
diff --git a/packages/cli-old/src/utils/renderTitle.ts b/packages/cli-old/src/utils/renderTitle.ts
new file mode 100644
index 00000000..d0f89738
--- /dev/null
+++ b/packages/cli-old/src/utils/renderTitle.ts
@@ -0,0 +1,20 @@
+import gradient from "gradient-string";
+
+import { TITLE_TEXT } from "~/consts.js";
+import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
+
+const proofTheme = {
+ purple: "#89216B",
+ lightPurple: "#D15ABB",
+ orange: "#FF595E",
+};
+
+export const proofGradient = gradient(Object.values(proofTheme));
+export const renderTitle = () => {
+ // resolves weird behavior where the ascii is offset
+ const pkgManager = getUserPkgManager();
+ if (pkgManager === "yarn" || pkgManager === "pnpm") {
+ console.log("");
+ }
+ console.log(proofGradient.multiline(TITLE_TEXT));
+};
diff --git a/packages/cli-old/src/utils/renderVersionWarning.ts b/packages/cli-old/src/utils/renderVersionWarning.ts
new file mode 100644
index 00000000..fd046831
--- /dev/null
+++ b/packages/cli-old/src/utils/renderVersionWarning.ts
@@ -0,0 +1,86 @@
+import { execSync } from "node:child_process";
+import https from "node:https";
+import chalk from "chalk";
+import * as semver from "semver";
+import * as p from "~/cli/prompts.js";
+
+import { cliName, npmName } from "~/consts.js";
+import { getVersion } from "./getProofKitVersion.js";
+import { getUserPkgManager } from "./getUserPkgManager.js";
+import { logger } from "./logger.js";
+
+export const renderVersionWarning = (npmVersion: string) => {
+ const currentVersion = getVersion();
+
+ // Check if current version is a pre-release (beta, alpha, etc.)
+ if (semver.prerelease(currentVersion)) {
+ logger.warn(` You are using a pre-release version of ${cliName}.`);
+ logger.warn(" Please report any bugs you encounter.");
+ } else if (semver.valid(currentVersion) && semver.valid(npmVersion) && semver.lt(currentVersion, npmVersion)) {
+ logger.warn(` You are using an outdated version of ${cliName}.`);
+ logger.warn(" Your version:", `${currentVersion}.`, "Latest version in the npm registry:", npmVersion);
+ logger.warn(" Please run the CLI with @latest to get the latest updates.");
+ }
+ console.log("");
+};
+
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root
+ * directory of this source tree.
+ * https://github.com/facebook/create-react-app/blob/main/packages/create-react-app/LICENSE
+ */
+interface DistTagsBody {
+ latest: string;
+}
+
+function checkForLatestVersion(): Promise {
+ return new Promise((resolve, reject) => {
+ https
+ .get("https://registry.npmjs.org/-/package/@proofkit/cli/dist-tags", (res) => {
+ if (res.statusCode === 200) {
+ let body = "";
+ res.on("data", (data) => {
+ body += data;
+ });
+ res.on("end", () => {
+ resolve((JSON.parse(body) as DistTagsBody).latest);
+ });
+ } else {
+ reject();
+ }
+ })
+ .on("error", () => {
+ // logger.error("Unable to check for latest version.");
+ reject();
+ });
+ });
+}
+
+export const getNpmVersion = async () =>
+ // `fetch` to the registry is faster than `npm view` so we try that first
+ checkForLatestVersion().catch(() => {
+ try {
+ return execSync("npm view proofkit version").toString().trim();
+ } catch {
+ return null;
+ }
+ });
+
+export const checkAndRenderVersionWarning = async () => {
+ const npmVersion = await getNpmVersion();
+ const currentVersion = getVersion();
+
+ // Only show warning if current version is valid, npm version is valid, and current is actually older
+ if (npmVersion && semver.valid(currentVersion) && semver.valid(npmVersion) && semver.lt(currentVersion, npmVersion)) {
+ const pkgManager = getUserPkgManager();
+ p.log.warn(
+ `${chalk.yellow(
+ `You are using an outdated version of ${cliName}.`,
+ )} Your version: ${currentVersion}. Latest version: ${npmVersion}.
+ Run ${chalk.magenta.bold(`${pkgManager} install ${npmName}@latest`)} to get the latest updates.`,
+ );
+ }
+ return { npmVersion, currentVersion };
+};
diff --git a/packages/cli-old/src/utils/ts-morph.ts b/packages/cli-old/src/utils/ts-morph.ts
new file mode 100644
index 00000000..92d58954
--- /dev/null
+++ b/packages/cli-old/src/utils/ts-morph.ts
@@ -0,0 +1,25 @@
+import path from "node:path";
+import { Project, type ReturnStatement, SyntaxKind } from "ts-morph";
+
+export { formatAndSaveSourceFiles } from "./formatting.js";
+
+export function ensureReturnStatementIsWrappedInFragment(returnStatement: ReturnStatement | undefined) {
+ const expression =
+ returnStatement?.getExpressionIfKind(SyntaxKind.ParenthesizedExpression)?.getExpression() ??
+ returnStatement?.getExpression();
+
+ if (expression?.isKind(SyntaxKind.JsxFragment)) {
+ return returnStatement;
+ }
+
+ returnStatement?.replaceWithText(`return <>${expression}>;`);
+ return returnStatement;
+}
+
+export function getNewProject(projectDir?: string) {
+ const project = new Project({
+ tsConfigFilePath: path.join(projectDir ?? process.cwd(), "tsconfig.json"),
+ });
+
+ return project;
+}
diff --git a/packages/cli-old/src/utils/validateAppName.ts b/packages/cli-old/src/utils/validateAppName.ts
new file mode 100644
index 00000000..b5b4e42e
--- /dev/null
+++ b/packages/cli-old/src/utils/validateAppName.ts
@@ -0,0 +1,22 @@
+import { removeTrailingSlash } from "./removeTrailingSlash.js";
+
+const validationRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
+
+//Validate a string against allowed package.json names
+export const validateAppName = (rawInput: string) => {
+ const input = removeTrailingSlash(rawInput);
+ const paths = input.split("/");
+
+ // If the first part is a @, it's a scoped package
+ const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@"));
+
+ let appName = paths.at(-1);
+ if (paths.findIndex((p) => p.startsWith("@")) !== -1) {
+ appName = paths.slice(indexOfDelimiter).join("/");
+ }
+
+ if (input === "." || validationRegExp.test(appName ?? "")) {
+ return;
+ }
+ return "Name must consist of only lowercase alphanumeric characters, '-', and '_'";
+};
diff --git a/packages/cli-old/src/utils/validateImportAlias.ts b/packages/cli-old/src/utils/validateImportAlias.ts
new file mode 100644
index 00000000..bd33ca61
--- /dev/null
+++ b/packages/cli-old/src/utils/validateImportAlias.ts
@@ -0,0 +1,6 @@
+export const validateImportAlias = (input: string) => {
+ if (input.startsWith(".") || input.startsWith("/")) {
+ return "Import alias can't start with '.' or '/'";
+ }
+ return;
+};
diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc
new file mode 100644
index 00000000..5ce7a9e0
--- /dev/null
+++ b/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc
@@ -0,0 +1,51 @@
+---
+description:
+globs:
+alwaysApply: true
+---
+# Next.js Framework Configuration
+
+This rule documents the Next.js framework setup and conventions used in this project.
+
+
+name: nextjs_framework
+description: Documents Next.js framework setup, routing conventions, and best practices
+filters:
+ - type: file_extension
+ pattern: "\\.(ts|tsx)$"
+ - type: directory
+ pattern: "src/(app|components)/"
+
+conventions:
+ routing:
+ - App Router is used (not Pages Router)
+ - Routes are defined in src/app directory
+ - Layout components should be named layout.tsx
+ - Page components should be named page.tsx
+ - Loading states should be in loading.tsx
+ - Error boundaries should be in error.tsx
+
+ components:
+ - React Server Components (RSC) are default
+ - Client components must be marked with "use client"
+ - Components live in src/components/
+ - Shared layouts in src/components/layouts/
+ - UI components in src/components/ui/
+
+ data_fetching:
+ - Server components fetch data directly
+ - Client components use React Query
+ - API routes defined in src/app/api/
+ - Server actions used for mutations
+
+frameworks:
+ next: "15.1.7"
+ react: "19.0.0-rc"
+ typescript: "^5"
+ mantine: "^7.17.0"
+ tanstack_query: "^5.59.0"
+
+metadata:
+ priority: high
+ version: 1.0
+
\ No newline at end of file
diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc
new file mode 100644
index 00000000..3b030fa5
--- /dev/null
+++ b/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc
@@ -0,0 +1,60 @@
+---
+description: |
+ This rule documents the package manager configuration and usage. It should be included when:
+ 1. Installing dependencies
+ 2. Running scripts
+ 3. Managing project packages
+ 4. Running development commands
+ 5. Executing build or test operations
+globs:
+ - "package.json"
+ - "package-lock.json"
+ - ".npmrc"
+alwaysApply: true
+---
+# Package Manager Configuration
+
+This rule documents the package manager setup and usage requirements.
+
+
+name: package_manager
+description: Documents package manager configuration and usage requirements
+
+configuration:
+ name: "npm"
+ version: "latest"
+ commands:
+ install: "npm install"
+ build: "npm run build"
+ dev: "npm run dev"
+ typegen: "npm run typegen"
+ typecheck: "npm run tsc"
+ notes: "Always use npm instead of yarn or pnpm for consistency"
+ dev_server_guidelines:
+ - "Never relaunch the dev server command if it may already be running"
+ - "Use npm run dev only when explicitly needed to start the server for the first time"
+ - "For code changes, just save the files and the server will automatically reload"
+
+examples:
+ - description: "Installing dependencies"
+ correct: "npm install"
+ incorrect:
+ - "pnpm install"
+ - "yarn install"
+
+ - description: "Running scripts"
+ correct: "npm run script-name"
+ incorrect:
+ - "pnpm run script-name"
+ - "yarn script-name"
+
+ - description: "Adding dependencies"
+ correct: "npm install package-name"
+ incorrect:
+ - "pnpm add package-name"
+ - "yarn add package-name"
+
+metadata:
+ priority: high
+ version: 1.0
+
\ No newline at end of file
diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc
new file mode 100644
index 00000000..d25da047
--- /dev/null
+++ b/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc
@@ -0,0 +1,65 @@
+---
+description: |
+globs:
+alwaysApply: true
+---
+---
+description: |
+ This rule documents the package manager configuration and usage. It should be included when:
+ 1. Installing dependencies
+ 2. Running scripts
+ 3. Managing project packages
+ 4. Running development commands
+ 5. Executing build or test operations
+globs:
+ - "package.json"
+ - "pnpm-lock.yaml"
+ - ".npmrc"
+alwaysApply: true
+---
+# Package Manager Configuration
+
+This rule documents the package manager setup and usage requirements.
+
+
+name: package_manager
+description: Documents package manager configuration and usage requirements
+
+configuration:
+ name: "pnpm"
+ version: "latest"
+ commands:
+ install: "pnpm install"
+ build: "pnpm build"
+ dev: "pnpm dev"
+ typegen: "pnpm typegen"
+ typecheck: "pnpm tsc"
+ notes: "Always use pnpm instead of npm or yarn for consistency"
+ dev_server_guidelines:
+ - "Never relaunch the dev server command if it may already be running"
+ - "Use pnpm dev only when explicitly needed to start the server for the first time"
+ - "For code changes, just save the files and the server will automatically reload"
+
+examples:
+ - description: "Installing dependencies"
+ correct: "pnpm install"
+ incorrect:
+ - "npm install"
+ - "yarn install"
+
+ - description: "Running scripts"
+ correct: "pnpm run script-name"
+ incorrect:
+ - "npm run script-name"
+ - "yarn script-name"
+
+ - description: "Adding dependencies"
+ correct: "pnpm add package-name"
+ incorrect:
+ - "npm install package-name"
+ - "yarn add package-name"
+
+metadata:
+ priority: high
+ version: 1.0
+
\ No newline at end of file
diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc
new file mode 100644
index 00000000..5672e80e
--- /dev/null
+++ b/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc
@@ -0,0 +1,60 @@
+---
+description: |
+ This rule documents the package manager configuration and usage. It should be included when:
+ 1. Installing dependencies
+ 2. Running scripts
+ 3. Managing project packages
+ 4. Running development commands
+ 5. Executing build or test operations
+globs:
+ - "package.json"
+ - "yarn.lock"
+ - ".yarnrc"
+alwaysApply: true
+---
+# Package Manager Configuration
+
+This rule documents the package manager setup and usage requirements.
+
+
+name: package_manager
+description: Documents package manager configuration and usage requirements
+
+configuration:
+ name: "yarn"
+ version: "latest"
+ commands:
+ install: "yarn install"
+ build: "yarn build"
+ dev: "yarn dev"
+ typegen: "yarn typegen"
+ typecheck: "yarn tsc"
+ notes: "Always use yarn instead of npm or pnpm for consistency"
+ dev_server_guidelines:
+ - "Never relaunch the dev server command if it may already be running"
+ - "Use yarn dev only when explicitly needed to start the server for the first time"
+ - "For code changes, just save the files and the server will automatically reload"
+
+examples:
+ - description: "Installing dependencies"
+ correct: "yarn install"
+ incorrect:
+ - "npm install"
+ - "pnpm install"
+
+ - description: "Running scripts"
+ correct: "yarn script-name"
+ incorrect:
+ - "npm run script-name"
+ - "pnpm run script-name"
+
+ - description: "Adding dependencies"
+ correct: "yarn add package-name"
+ incorrect:
+ - "npm install package-name"
+ - "pnpm add package-name"
+
+metadata:
+ priority: high
+ version: 1.0
+
\ No newline at end of file
diff --git a/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc b/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc
new file mode 100644
index 00000000..061da499
--- /dev/null
+++ b/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc
@@ -0,0 +1,88 @@
+---
+description: |
+ This rule documents how to manage and organize Cursor rules. It should be included when:
+ 1. Creating or modifying Cursor rules
+ 2. Organizing documentation for the codebase
+ 3. Setting up new development patterns
+ 4. Adding project-wide conventions
+ 5. Managing rule file locations
+ 6. Updating rule descriptions or globs
+ 7. Working with .cursor directory structure
+globs:
+ - ".cursor/rules/*.mdc"
+ - ".cursor/config/*.json"
+ - ".cursor/settings/*.json"
+alwaysApply: true
+---
+# Cursor Rules Location
+
+Rules for placing and organizing Cursor rule files in the repository.
+
+
+name: cursor_rules_location
+description: Standards for placing Cursor rule files in the correct directory
+filters:
+ # Match any .mdc files
+ - type: file_extension
+ pattern: "\\.mdc$"
+ # Match files that look like Cursor rules
+ - type: content
+ pattern: "(?s).*?"
+ # Match file creation events
+ - type: event
+ pattern: "file_create"
+
+actions:
+ - type: reject
+ conditions:
+ - pattern: "^(?!\\.\\/\\.cursor\\/rules\\/.*\\.mdc$)"
+ message: "Cursor rule files (.mdc) must be placed in the .cursor/rules directory"
+
+ - type: suggest
+ message: |
+ When creating Cursor rules:
+
+ 1. Always place rule files in PROJECT_ROOT/.cursor/rules/:
+ ```
+ .cursor/rules/
+ ├── your-rule-name.mdc
+ ├── another-rule.mdc
+ └── ...
+ ```
+
+ 2. Follow the naming convention:
+ - Use kebab-case for filenames
+ - Always use .mdc extension
+ - Make names descriptive of the rule's purpose
+
+ 3. Directory structure:
+ ```
+ PROJECT_ROOT/
+ ├── .cursor/
+ │ └── rules/
+ │ ├── your-rule-name.mdc
+ │ └── ...
+ └── ...
+ ```
+
+ 4. Never place rule files:
+ - In the project root
+ - In subdirectories outside .cursor/rules
+ - In any other location
+ - Inside of the cursor-rules.mdc file
+
+examples:
+ - input: |
+ # Bad: Rule file in wrong location
+ rules/my-rule.mdc
+ my-rule.mdc
+ .rules/my-rule.mdc
+
+ # Good: Rule file in correct location
+ .cursor/rules/my-rule.mdc
+ output: "Correctly placed Cursor rule file"
+
+metadata:
+ priority: high
+ version: 1.0
+
\ No newline at end of file
diff --git a/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc b/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc
new file mode 100644
index 00000000..dd6d6716
--- /dev/null
+++ b/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc
@@ -0,0 +1,176 @@
+---
+description: |
+ This rule provides guidance for working with the FileMaker Data API in this project. It should be included when:
+ 1. Working with database operations or data fetching
+ 2. Encountering database-related errors or type issues
+ 3. Making changes to FileMaker schemas or layouts
+ 4. Implementing new data access patterns
+ 5. Discussing alternative data storage solutions
+ 6. Working with server-side API routes or actions
+globs:
+ - "src/**/*.ts"
+ - "src/**/*.tsx"
+ - "**/fmschema.config.mjs"
+ - "src/**/actions/*.ts"
+alwaysApply: true
+---
+# FileMaker Data API Integration
+
+This rule documents how the FileMaker Data API is integrated and used in the project.
+
+
+name: filemaker_api
+description: Documents FileMaker Data API integration patterns and conventions. FileMaker is the ONLY data source for this application - no SQL or other databases should be used.
+filters:
+ - type: file_extension
+ pattern: "\\.(ts|tsx)$"
+ - type: directory
+ pattern: "src/server/"
+ - type: content
+ pattern: "(@proofkit/cli|ZodError|typegen)"
+
+data_source_policy:
+ exclusive_source: "FileMaker Data API"
+ prohibited:
+ - "SQL databases"
+ - "NoSQL databases"
+ - "Local storage for persistent data"
+ - "Direct file system storage"
+ reason: "All data operations must go through FileMaker to maintain data integrity and business logic"
+
+troubleshooting:
+ priority_order:
+ - "ALWAYS run `{package-manager} typegen` first for ANY data loading issues"
+ - "DO NOT check environment variables unless you have a specific error message pointing to them"
+ - "Check for FileMaker schema changes"
+ - "Verify type definitions match current schema"
+ - "Review Zod validation errors"
+ rationale: "Most data loading issues are resolved by running typegen. Environment variables are rarely the cause of data loading problems and should not be investigated unless specific error messages indicate an authentication or connection issue."
+
+conventions:
+ api_setup:
+ - Uses @proofkit/fmdapi package version ^5.0.0
+ - Configuration in fmschema.config.mjs
+ - Environment variables in .env for connection details
+ - Type generation via `{package-manager} typegen` command
+
+ data_access:
+ - ALL data operations MUST use FileMaker Data API
+ - Server-side only API calls via @proofkit/fmdapi
+ - Type-safe database operations
+ - Centralized error handling
+ - Connection pooling and session management
+ - No direct database connections outside FileMaker
+
+ data_operations:
+ create:
+ - Use layout.create({ fieldData: {...} })
+ - Validate input against Zod schemas
+ - Returns recordId of created record
+ - Handle duplicates via FileMaker business logic
+ read:
+ - Use layout.get({ recordId }) for single record by ID
+ - Use layout.find({ query, limit, offset, sort }) for multiple records
+ - Use layout.maybeFindFirst({ query }) for optional single record
+ - Support for complex queries and sorting
+ update:
+ - Use layout.update({ recordId, fieldData })
+ - Follow FileMaker field naming conventions
+ - Respect FileMaker validation rules
+ delete:
+ - Use layout.delete({ recordId })
+ - Respect FileMaker deletion rules
+ - Handle cascading deletes via FileMaker
+ query_options:
+ - Limit and offset for pagination
+ - Sort by multiple fields with ascend/descend
+ - Complex query criteria with operators (==, *, etc.)
+ - Optional type-safe responses with Zod validation
+
+ security:
+ - Credentials stored in environment variables
+ - No direct client-side FM API access
+ - API routes validate authentication
+ - Data sanitization before queries
+ - All database access through FileMaker only
+
+type_generation:
+ process:
+ - "IMPORTANT: Running `{package-manager} typegen` solves almost all data loading problems"
+ - "Run `{package-manager} typegen` after any FileMaker schema changes"
+ - "Run `{package-manager} typegen` as first step when troubleshooting data issues"
+ - "Types are generated from FileMaker database schema"
+ - "Generated types are used in server actions and components"
+ - "Zod schemas validate runtime data against types"
+
+ common_issues:
+ schema_changes:
+ symptoms:
+ - "No data appearing in tables"
+ - "ZodError during runtime"
+ - "Missing or renamed fields"
+ - "Type mismatches in responses"
+ - "Empty query results"
+ solution: "ALWAYS run `{package-manager} typegen && {package-manager} tsc` first"
+ important_note: "Do NOT check environment variables as a cause for data loading problems unless you have a specific known error that points to environment variables. Most data loading issues are resolved by running typegen."
+
+ field_types:
+ symptoms:
+ - "Unexpected null values"
+ - "Type conversion errors"
+ - "Invalid date formats"
+ solution: "Update Zod schemas and type definitions"
+
+ security_notes:
+ - "Never display, log, or commit environment variables"
+ - "Never check environment variable values directly"
+ - "Keep .env files out of version control"
+ - "When troubleshooting, only verify if variables exist, never their values"
+
+patterns:
+ - Server actions wrap FM API calls
+ - Type definitions generated from FM schema
+ - Error boundaries for FM API errors
+ - Rate limiting on API routes
+ - Caching strategies for frequent queries
+
+dependencies:
+ fmdapi: "@proofkit/fmdapi@^5.0.0"
+ proofkit: "@proofkit/cli@^1.0.0"
+
+keywords:
+ database:
+ - "FileMaker"
+ - "FMREST"
+ - "Database schema"
+ - "Field types"
+ - "Type generation"
+ - "Schema changes"
+ - "Exclusive data source"
+ - "No SQL"
+ - "FileMaker only"
+ - "Data API"
+ errors:
+ - "ZodError"
+ - "TypeError"
+ - "ValidationError"
+ - "Missing field"
+ - "Runtime error"
+ commands:
+ - "typegen"
+ - "tsc"
+ - "type checking"
+ - "schema update"
+ operations:
+ - "FM.create"
+ - "FM.find"
+ - "FM.get"
+ - "FM.update"
+ - "FM.delete"
+ - "FileMaker layout"
+ - "FileMaker query"
+
+metadata:
+ priority: high
+ version: 1.0
+
\ No newline at end of file
diff --git a/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc b/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc
new file mode 100644
index 00000000..797fd3cc
--- /dev/null
+++ b/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc
@@ -0,0 +1,240 @@
+---
+description: |
+globs:
+alwaysApply: false
+---
+# Troubleshooting and Maintenance Patterns
+
+This rule documents common issues, error patterns, and their solutions in the project.
+
+
+name: troubleshooting_patterns
+description: Documents common runtime errors, type errors, and solutions. All data operations MUST use FileMaker Data API exclusively.
+filters:
+ - type: file_extension
+ pattern: "\\.(ts|tsx)$"
+ - type: content
+ pattern: "(Error|error|ZodError|TypeError|ValidationError|@proofkit/fmdapi)"
+
+initial_debugging_steps:
+ priority: "ALWAYS run `{package-manager} typegen` first for any data-related issues"
+ steps:
+ - "Run `{package-manager} typegen` to ensure types match FileMaker schema"
+ - "Check if error persists after typegen"
+ - "If error persists, check console for exact error messages"
+ - "Look for patterns in the troubleshooting guide below"
+ common_console_errors:
+ zod_errors:
+ pattern: "ZodError: [path] invalid_type..."
+ likely_cause: "Field name mismatch or missing field"
+ example: "ZodError: nameFirst expected string, got undefined"
+ solution: "Run typegen first, then check field names in FileMaker schema"
+ type_errors:
+ pattern: "TypeError: Cannot read property 'X' of undefined"
+ likely_cause: "Accessing field before data is loaded or field name mismatch"
+ solution: "Run typegen first, then add null checks or loading states"
+ network_errors:
+ pattern: "Failed to fetch" or "Network error"
+ likely_cause: "FileMaker connection issues"
+ solution: "Run typegen first, then check FileMaker server status and credentials"
+
+data_source_validation:
+ requirement: "All data operations must use FileMaker Data API exclusively"
+ first_step_for_data_issues: "ALWAYS run `{package-manager} typegen` first"
+ common_mistakes:
+ - "Attempting to use SQL queries"
+ - "Adding direct database connections"
+ - "Using local storage for persistent data"
+ - "Implementing alternative data stores"
+ - "Skipping typegen after FileMaker schema changes"
+ - "Using incorrect field names from old schema"
+ correct_approach:
+ - "Run typegen first"
+ - "Use @proofkit/fmdapi for all data operations"
+ - "Follow FileMaker layout and field conventions"
+ - "Use layout.create, layout.find, layout.get, layout.update, layout.delete"
+ - "Use layout.maybeFindFirst for optional records"
+
+error_patterns:
+ field_name_mismatches:
+ symptoms:
+ - "ZodError: invalid_type at path [fieldName]"
+ - "Property 'X' does not exist on type 'Y'"
+ - "TypeScript errors about missing properties"
+ common_examples:
+ - "nameFirst vs firstName"
+ - "lastName vs nameLast"
+ - "postalCode vs postal_code"
+ - "phoneNumber vs phone"
+ cause: "Mismatch between component field names and FileMaker schema"
+ solution:
+ steps:
+ - "Run `{package-manager} typegen` to update types"
+ - "Look at generated types in src/config/schemas/filemaker/"
+ - "Update component field names to match schema"
+ - "Check console for exact field name in error"
+ files_to_check:
+ - "src/config/schemas/filemaker/*.ts"
+ - "Component files using the fields"
+
+ zod_validation_errors:
+ symptoms:
+ - "Runtime ZodError: invalid_type"
+ - "Zod schema validation failed"
+ - "Property not found in schema"
+ - "Unexpected field in response"
+ cause: "FileMaker database schema changes not reflected in TypeScript types"
+ solution:
+ steps:
+ - "Run `{package-manager} typegen` to regenerate types from FileMaker schema"
+ - "Run `{package-manager} tsc` to identify type mismatches"
+ - "Check console for exact error message"
+ - "Update affected components and server actions"
+ commands:
+ - "{package-manager} typegen"
+ - "{package-manager} tsc"
+ files_to_check:
+ - "src/server/actions/*"
+ - "src/server/schema/*"
+ - "fmschema.config.mjs"
+
+ filemaker_connection:
+ symptoms:
+ - "ETIMEDOUT connecting to FileMaker"
+ - "Invalid FileMaker credentials"
+ - "Session token expired"
+ - "Layout not found"
+ - "Field not found in layout"
+ - "Invalid find criteria"
+ - "No data appearing or queries returning empty"
+ cause: "FileMaker connection, authentication, or query issues"
+ solution:
+ steps:
+ - "Run `{package-manager} typegen` to ensure schema is up to date"
+ - "Check FileMaker Server status"
+ - "Validate credentials and permissions"
+ - "Note: As an AI, you cannot directly check environment variables - always ask the user to verify them if this is determined to be the issue"
+ - "Verify layout names and field access"
+ - "Check FileMaker query syntax"
+ files_to_check:
+ - "src/server/lib/fm.ts"
+ - "fmschema.config.mjs"
+
+ data_access_errors:
+ symptoms:
+ - "Invalid operation on FileMaker record"
+ - "Record not found"
+ - "Insufficient permissions"
+ - "Invalid find request"
+ cause: "Incorrect FileMaker Data API usage or permissions"
+ solution:
+ steps:
+ - "Run `{package-manager} typegen` to ensure schema is up to date"
+ - "Verify FileMaker layout privileges"
+ - "Check record existence before operations"
+ - "Validate find criteria format"
+ - "Use proper FM API methods"
+ files_to_check:
+ - "src/server/actions/*"
+ - "src/server/lib/fm.ts"
+
+ type_errors:
+ symptoms:
+ - "Type ... is not assignable to type ..."
+ - "Property ... does not exist on type ..."
+ - "Argument of type ... is not assignable"
+ cause: "Mismatch between FileMaker schema and TypeScript types"
+ solution:
+ steps:
+ - "Run `{package-manager} typegen` to regenerate types"
+ - "Run `{package-manager} tsc` to identify type mismatches"
+ - "Update type definitions if needed"
+ - "Check for null/undefined handling"
+ commands:
+ - "{package-manager} typegen && {package-manager} tsc"
+
+ data_sync_issues:
+ symptoms:
+ - "Missing fields in table"
+ - "Unexpected null values"
+ - "Fields showing as blank"
+ - "Type mismatches between FM and frontend"
+ first_step: "ALWAYS run `{package-manager} typegen` first"
+ cause: "Mismatch between FileMaker schema and TypeScript types, or outdated type definitions"
+ solution:
+ steps:
+ - "Run `{package-manager} typegen` to regenerate types from FileMaker schema"
+ - "Check for any type errors in the console"
+ - "Verify field names match exactly between FM and generated types"
+ - "Update components if field names have changed"
+ commands:
+ - "{package-manager} typegen"
+ - "{package-manager} tsc"
+ files_to_check:
+ - "src/config/schemas/filemaker/*.ts"
+ - "fmschema.config.mjs"
+
+maintenance_tasks:
+ schema_sync:
+ description: "Keep FileMaker schema and TypeScript types in sync"
+ frequency: "After any FileMaker schema changes"
+ steps:
+ - "Run typegen to update types"
+ - "Run TypeScript compiler"
+ - "Update affected components"
+ impact: "Prevents runtime errors and type mismatches"
+
+ type_checking:
+ description: "Regular type checking for early error detection"
+ frequency: "Before deployments and after schema changes"
+ commands:
+ - "{package-manager} tsc --noEmit"
+ impact: "Catches type errors before runtime"
+
+keywords:
+ errors:
+ - "ZodError"
+ - "TypeError"
+ - "ValidationError"
+ - "Schema mismatch"
+ - "Type mismatch"
+ - "Runtime error"
+ - "Database schema"
+ - "Type generation"
+ - "FileMaker fields"
+ - "Missing property"
+ - "Invalid type"
+ - "Layout not found"
+ - "Field not found"
+ - "Invalid find request"
+ solutions:
+ - "typegen"
+ - "tsc"
+ - "type checking"
+ - "schema update"
+ - "validation fix"
+ - "error handling"
+ - "FM API methods"
+ - "FileMaker layout"
+ operations:
+ - "layout.create"
+ - "layout.find"
+ - "layout.get"
+ - "layout.update"
+ - "layout.delete"
+ - "layout.maybeFindFirst"
+ - "recordId"
+ - "fieldData"
+ - "query parameters"
+ - "sort options"
+ data_source:
+ - "FileMaker only"
+ - "No SQL"
+ - "FM Data API"
+ - "Exclusive data source"
+ - "@proofkit/fmdapi"
+
+metadata:
+ priority: high
+ version: 1.0
+
\ No newline at end of file
diff --git a/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc b/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc
new file mode 100644
index 00000000..78ec63ad
--- /dev/null
+++ b/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc
@@ -0,0 +1,57 @@
+---
+description:
+globs:
+alwaysApply: false
+---
+# UI Components and Styling
+
+This rule documents the UI component library and styling conventions used in the project.
+
+
+name: ui_components
+description: Documents UI component library usage and styling conventions
+filters:
+ - type: file_extension
+ pattern: "\\.(ts|tsx)$"
+ - type: directory
+ pattern: "src/components/"
+ - type: content
+ pattern: "@mantine/"
+
+conventions:
+ component_library:
+ - Mantine v7 as primary UI framework
+ - Tabler icons for iconography
+ - Mantine React Table for data grids
+ - Custom components extend Mantine base
+
+ styling:
+ - PostCSS for processing
+ - Mantine theme customization
+ - CSS modules for component styles
+ - CSS variables for theming
+
+ components:
+ - Atomic design principles
+ - Consistent prop interfaces
+ - Accessibility first
+ - Responsive design patterns
+
+ forms:
+ - React Hook Form for form state
+ - Zod for validation schemas
+ - Mantine form components
+ - Custom form layouts
+
+dependencies:
+ mantine_core: "^7.17.0"
+ mantine_hooks: "^7.17.0"
+ mantine_dates: "^7.17.0"
+ mantine_notifications: "^7.17.0"
+ react_hook_form: "^7.54.2"
+ zod: "^3.24.2"
+
+metadata:
+ priority: high
+ version: 1.0
+
\ No newline at end of file
diff --git a/packages/cli-old/template/extras/config/drizzle-config-mysql.ts b/packages/cli-old/template/extras/config/drizzle-config-mysql.ts
new file mode 100644
index 00000000..1f71d754
--- /dev/null
+++ b/packages/cli-old/template/extras/config/drizzle-config-mysql.ts
@@ -0,0 +1,12 @@
+import { type Config } from "drizzle-kit";
+
+import { env } from "~/env";
+
+export default {
+ schema: "./src/server/db/schema.ts",
+ dialect: "mysql",
+ dbCredentials: {
+ url: env.DATABASE_URL,
+ },
+ tablesFilter: ["project1_*"],
+} satisfies Config;
diff --git a/packages/cli-old/template/extras/config/drizzle-config-postgres.ts b/packages/cli-old/template/extras/config/drizzle-config-postgres.ts
new file mode 100644
index 00000000..d2a21ed7
--- /dev/null
+++ b/packages/cli-old/template/extras/config/drizzle-config-postgres.ts
@@ -0,0 +1,12 @@
+import { type Config } from "drizzle-kit";
+
+import { env } from "~/env";
+
+export default {
+ schema: "./src/server/db/schema.ts",
+ dialect: "postgresql",
+ dbCredentials: {
+ url: env.DATABASE_URL,
+ },
+ tablesFilter: ["project1_*"],
+} satisfies Config;
diff --git a/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts b/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts
new file mode 100644
index 00000000..34f8fa24
--- /dev/null
+++ b/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts
@@ -0,0 +1,12 @@
+import { type Config } from "drizzle-kit";
+
+import { env } from "~/env";
+
+export default {
+ schema: "./src/server/db/schema.ts",
+ dialect: "sqlite",
+ dbCredentials: {
+ url: env.DATABASE_URL,
+ },
+ tablesFilter: ["project1_*"],
+} satisfies Config;
diff --git a/packages/cli-old/template/extras/config/fmschema.config.mjs b/packages/cli-old/template/extras/config/fmschema.config.mjs
new file mode 100644
index 00000000..660edd23
--- /dev/null
+++ b/packages/cli-old/template/extras/config/fmschema.config.mjs
@@ -0,0 +1,9 @@
+/** @type {import("@proofkit/fmdapi/dist/utils/typegen/types.d.ts").GenerateSchemaOptions} */
+export const config = {
+ clientSuffix: "Layout",
+ schemas: [
+ // add your layouts and name schemas here
+ ],
+ clearOldFiles: true,
+ path: "./src/config/schemas/filemaker",
+};
diff --git a/packages/cli-old/template/extras/config/get-query-client.ts b/packages/cli-old/template/extras/config/get-query-client.ts
new file mode 100644
index 00000000..44598cba
--- /dev/null
+++ b/packages/cli-old/template/extras/config/get-query-client.ts
@@ -0,0 +1,6 @@
+import { QueryClient } from "@tanstack/react-query";
+import { cache } from "react";
+
+// cache() is scoped per request, so we don't leak data between requests
+const getQueryClient = cache(() => new QueryClient());
+export default getQueryClient;
diff --git a/packages/cli-old/template/extras/config/postcss.config.cjs b/packages/cli-old/template/extras/config/postcss.config.cjs
new file mode 100644
index 00000000..4cdb2f43
--- /dev/null
+++ b/packages/cli-old/template/extras/config/postcss.config.cjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ tailwindcss: {},
+ },
+};
+
+module.exports = config;
diff --git a/packages/cli-old/template/extras/config/query-provider-vite.tsx b/packages/cli-old/template/extras/config/query-provider-vite.tsx
new file mode 100644
index 00000000..5af4ad27
--- /dev/null
+++ b/packages/cli-old/template/extras/config/query-provider-vite.tsx
@@ -0,0 +1,17 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+
+const queryClient = new QueryClient();
+
+export default function QueryProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/config/query-provider.tsx b/packages/cli-old/template/extras/config/query-provider.tsx
new file mode 100644
index 00000000..2afa87bd
--- /dev/null
+++ b/packages/cli-old/template/extras/config/query-provider.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+
+import getQueryClient from "./get-query-client";
+
+export default function QueryProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const queryClient = getQueryClient();
+
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/emailProviders/none/email.tsx b/packages/cli-old/template/extras/emailProviders/none/email.tsx
new file mode 100644
index 00000000..21106a19
--- /dev/null
+++ b/packages/cli-old/template/extras/emailProviders/none/email.tsx
@@ -0,0 +1,24 @@
+import { AuthCodeEmail } from "@/emails/auth-code";
+import { render } from "@react-email/render";
+
+export async function sendEmail({
+ to,
+ code,
+ type,
+}: {
+ to: string;
+ code: string;
+ type: "verification" | "password-reset";
+}) {
+ // this is the HTML body of the email to be send
+ const body = await render(
+
+ );
+ const subject =
+ type === "verification" ? "Verify Your Email" : "Reset Your Password";
+
+ // TODO: Customize this function to actually send the email to your users
+ // Learn more: https://proofkit.dev/auth/fm-addon
+ console.warn("TODO: Customize this function to actually send to your users");
+ console.log(`To ${to}: Your ${type} code is ${code}`);
+}
diff --git a/packages/cli-old/template/extras/emailProviders/plunk/email.tsx b/packages/cli-old/template/extras/emailProviders/plunk/email.tsx
new file mode 100644
index 00000000..ef94053e
--- /dev/null
+++ b/packages/cli-old/template/extras/emailProviders/plunk/email.tsx
@@ -0,0 +1,27 @@
+import { AuthCodeEmail } from "@/emails/auth-code";
+import { render } from "@react-email/render";
+
+import { plunk } from "../services/plunk";
+
+export async function sendEmail({
+ to,
+ code,
+ type,
+}: {
+ to: string;
+ code: string;
+ type: "verification" | "password-reset";
+}) {
+ // this is the HTML body of the email to be send
+ const body = await render(
+
+ );
+ const subject =
+ type === "verification" ? "Verify Your Email" : "Reset Your Password";
+
+ await plunk.emails.send({
+ to,
+ subject,
+ body,
+ });
+}
diff --git a/packages/cli-old/template/extras/emailProviders/plunk/service.ts b/packages/cli-old/template/extras/emailProviders/plunk/service.ts
new file mode 100644
index 00000000..9f6a3ca6
--- /dev/null
+++ b/packages/cli-old/template/extras/emailProviders/plunk/service.ts
@@ -0,0 +1,4 @@
+import { env } from "@/config/env";
+import Plunk from "@plunk/node";
+
+export const plunk = new Plunk(env.PLUNK_API_KEY);
diff --git a/packages/cli-old/template/extras/emailProviders/resend/email.tsx b/packages/cli-old/template/extras/emailProviders/resend/email.tsx
new file mode 100644
index 00000000..5ca905b8
--- /dev/null
+++ b/packages/cli-old/template/extras/emailProviders/resend/email.tsx
@@ -0,0 +1,24 @@
+import { AuthCodeEmail } from "@/emails/auth-code";
+
+import { resend } from "../services/resend";
+
+export async function sendEmail({
+ to,
+ code,
+ type,
+}: {
+ to: string;
+ code: string;
+ type: "verification" | "password-reset";
+}) {
+ const subject =
+ type === "verification" ? "Verify Your Email" : "Reset Your Password";
+
+ await resend.emails.send({
+ // TODO: Change this to our own email after verifying your domain with Resend
+ from: "ProofKit ",
+ to,
+ subject,
+ react: ,
+ });
+}
diff --git a/packages/cli-old/template/extras/emailProviders/resend/service.ts b/packages/cli-old/template/extras/emailProviders/resend/service.ts
new file mode 100644
index 00000000..9af08cd1
--- /dev/null
+++ b/packages/cli-old/template/extras/emailProviders/resend/service.ts
@@ -0,0 +1,4 @@
+import { env } from "@/config/env";
+import { Resend } from "resend";
+
+export const resend = new Resend(env.RESEND_API_KEY);
diff --git a/packages/cli-old/template/extras/emailTemplates/auth-code.tsx b/packages/cli-old/template/extras/emailTemplates/auth-code.tsx
new file mode 100644
index 00000000..e09789c5
--- /dev/null
+++ b/packages/cli-old/template/extras/emailTemplates/auth-code.tsx
@@ -0,0 +1,137 @@
+import { Body, Container, Head, Heading, Html, Img, Section, Text } from "@react-email/components";
+
+interface AuthCodeEmailProps {
+ validationCode: string;
+ type: "verification" | "password-reset";
+}
+
+export const AuthCodeEmail = ({ validationCode, type }: AuthCodeEmailProps) => (
+
+
+
+
+
+ {type === "verification" ? "Verify Your Email" : "Reset Your Password"}
+
+ Enter the following code to {type === "verification" ? "verify your email" : "reset your password"}
+
+
+ If you did not request this code, you can ignore this email.
+
+
+
+);
+
+AuthCodeEmail.PreviewProps = {
+ validationCode: "D7CU4GOV",
+ type: "verification",
+} as AuthCodeEmailProps;
+
+export default AuthCodeEmail;
+
+const main = {
+ backgroundColor: "#ffffff",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+};
+
+const container = {
+ backgroundColor: "#ffffff",
+ border: "1px solid #eee",
+ borderRadius: "5px",
+ boxShadow: "0 5px 10px rgba(20,50,70,.2)",
+ marginTop: "20px",
+ maxWidth: "360px",
+ margin: "0 auto",
+ padding: "68px 0 130px",
+};
+
+const logo: React.CSSProperties = {
+ margin: "0 auto",
+};
+
+const tertiary = {
+ color: "#0a85ea",
+ fontSize: "11px",
+ fontWeight: 700,
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ height: "16px",
+ letterSpacing: "0",
+ lineHeight: "16px",
+ margin: "16px 8px 8px 8px",
+ textTransform: "uppercase" as const,
+ textAlign: "center" as const,
+};
+
+const secondary = {
+ color: "#000",
+ display: "inline-block",
+ fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif",
+ fontSize: "20px",
+ fontWeight: 500,
+ lineHeight: "24px",
+ marginBottom: "0",
+ marginTop: "0",
+ textAlign: "center" as const,
+ padding: "0 40px",
+};
+
+const codeContainer = {
+ background: "rgba(0,0,0,.05)",
+ borderRadius: "4px",
+ margin: "16px auto 14px",
+ verticalAlign: "middle",
+ width: "280px",
+};
+
+const code = {
+ color: "#000",
+ display: "inline-block",
+ fontFamily: "HelveticaNeue-Bold",
+ fontSize: "32px",
+ fontWeight: 700,
+ letterSpacing: "6px",
+ lineHeight: "40px",
+ paddingBottom: "8px",
+ paddingTop: "8px",
+ margin: "0 auto",
+ width: "100%",
+ textAlign: "center" as const,
+};
+
+const paragraph = {
+ color: "#444",
+ fontSize: "15px",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ letterSpacing: "0",
+ lineHeight: "23px",
+ padding: "0 40px",
+ margin: "0",
+ textAlign: "center" as const,
+};
+
+const link = {
+ color: "#444",
+ textDecoration: "underline",
+};
+
+const footer = {
+ color: "#000",
+ fontSize: "12px",
+ fontWeight: 800,
+ letterSpacing: "0",
+ lineHeight: "23px",
+ margin: "0",
+ marginTop: "20px",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ textAlign: "center" as const,
+ textTransform: "uppercase" as const,
+};
diff --git a/packages/cli-old/template/extras/emailTemplates/generic.tsx b/packages/cli-old/template/extras/emailTemplates/generic.tsx
new file mode 100644
index 00000000..5c4046aa
--- /dev/null
+++ b/packages/cli-old/template/extras/emailTemplates/generic.tsx
@@ -0,0 +1,113 @@
+import { Body, Button, Container, Head, Heading, Hr, Html, Img, Section, Text } from "@react-email/components";
+
+export interface GenericEmailProps {
+ title?: string;
+ description?: string;
+ ctaText?: string;
+ ctaHref?: string;
+ footer?: string;
+}
+
+export const GenericEmail = ({ title, description, ctaText, ctaHref, footer }: GenericEmailProps) => (
+
+
+
+
+
+
+ {title ? {title} : null}
+
+ {description ? {description} : null}
+
+ {ctaText && ctaHref ? (
+
+ ) : null}
+
+ {(title || description || (ctaText && ctaHref)) &&
}
+
+ {footer ? {footer} : null}
+
+
+
+);
+
+GenericEmail.PreviewProps = {
+ title: "Welcome to ProofKit",
+ description: "Thanks for trying ProofKit. This is a sample email template you can customize.",
+ ctaText: "Get Started",
+ ctaHref: "https://proofkit.dev",
+ footer: "You received this email because you signed up for updates.",
+} as GenericEmailProps;
+
+export default GenericEmail;
+
+const styles = {
+ main: {
+ backgroundColor: "#ffffff",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ },
+ container: {
+ backgroundColor: "#ffffff",
+ border: "1px solid #eee",
+ borderRadius: "5px",
+ boxShadow: "0 5px 10px rgba(20,50,70,.2)",
+ marginTop: "20px",
+ maxWidth: "520px",
+ margin: "0 auto",
+ padding: "48px 32px 36px",
+ } as React.CSSProperties,
+ logo: {
+ margin: "0 auto 12px",
+ display: "block",
+ } as React.CSSProperties,
+ title: {
+ color: "#111827",
+ fontSize: "22px",
+ fontWeight: 600,
+ lineHeight: "28px",
+ margin: "8px 0 4px",
+ textAlign: "center" as const,
+ },
+ description: {
+ color: "#374151",
+ fontSize: "15px",
+ lineHeight: "22px",
+ margin: "8px 0 0",
+ textAlign: "center" as const,
+ },
+ ctaSection: {
+ textAlign: "center" as const,
+ marginTop: "20px",
+ },
+ ctaButton: {
+ backgroundColor: "#0a85ea",
+ color: "#fff",
+ fontSize: "14px",
+ fontWeight: 600,
+ lineHeight: "20px",
+ textDecoration: "none",
+ display: "inline-block",
+ padding: "10px 16px",
+ borderRadius: "6px",
+ } as React.CSSProperties,
+ hr: {
+ borderColor: "#e5e7eb",
+ margin: "24px 0 12px",
+ },
+ footer: {
+ color: "#6b7280",
+ fontSize: "12px",
+ lineHeight: "18px",
+ textAlign: "center" as const,
+ },
+};
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts
new file mode 100644
index 00000000..49191dfa
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts
@@ -0,0 +1,97 @@
+"use server";
+
+import {
+ createEmailVerificationRequest,
+ sendVerificationEmail,
+ setEmailVerificationRequestCookie,
+} from "@/server/auth/utils/email-verification";
+import {
+ verifyPasswordHash,
+ verifyPasswordStrength,
+} from "@/server/auth/utils/password";
+import {
+ createSession,
+ generateSessionToken,
+ getCurrentSession,
+ invalidateUserSessions,
+ setSessionTokenCookie,
+} from "@/server/auth/utils/session";
+import {
+ checkEmailAvailability,
+ updateUserPassword,
+ validateLogin,
+} from "@/server/auth/utils/user";
+import { actionClient } from "@/server/safe-action";
+import { redirect } from "next/navigation";
+
+import { updateEmailSchema, updatePasswordSchema } from "./schema";
+
+export const updateEmailAction = actionClient
+ .schema(updateEmailSchema)
+ .action(async ({ parsedInput }) => {
+ const { session, user } = await getCurrentSession();
+ if (session === null) {
+ return {
+ message: "Not authenticated",
+ };
+ }
+
+ const { email } = parsedInput;
+
+ const emailAvailable = await checkEmailAvailability(email);
+ if (!emailAvailable) {
+ return {
+ error: "This email is already used",
+ };
+ }
+
+ const verificationRequest = await createEmailVerificationRequest(
+ user.id,
+ email
+ );
+ await sendVerificationEmail(
+ verificationRequest.email,
+ verificationRequest.code
+ );
+ await setEmailVerificationRequestCookie(verificationRequest);
+ return redirect("/auth/verify-email");
+ });
+
+export const updatePasswordAction = actionClient
+ .schema(updatePasswordSchema)
+ .action(async ({ parsedInput }) => {
+ const { confirmNewPassword, currentPassword, newPassword } = parsedInput;
+
+ const { session, user } = await getCurrentSession();
+ if (session === null) {
+ return {
+ error: "Not authenticated",
+ };
+ }
+
+ const strongPassword = await verifyPasswordStrength(newPassword);
+ if (!strongPassword) {
+ return {
+ error: "Weak password",
+ };
+ }
+
+ const validPassword = Boolean(
+ await validateLogin(user.email, currentPassword)
+ );
+ if (!validPassword) {
+ return {
+ error: "Incorrect password",
+ };
+ }
+
+ await invalidateUserSessions(user.id);
+ await updateUserPassword(user.id, newPassword);
+
+ const sessionToken = generateSessionToken();
+ const newSession = await createSession(sessionToken, user.id);
+ await setSessionTokenCookie(sessionToken, newSession.expiresAt);
+ return {
+ message: "Password updated",
+ };
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx
new file mode 100644
index 00000000..76431716
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx
@@ -0,0 +1,29 @@
+import { getCurrentSession } from "@/server/auth/utils/session";
+import { Anchor, Container, Paper, Stack, Text, Title } from "@mantine/core";
+import { redirect } from "next/navigation";
+
+import UpdateEmailForm from "./profile-form";
+import UpdatePasswordForm from "./reset-password-form";
+
+// import EmailVerificationForm from "./email-verification-form";
+
+export default async function Page() {
+ const { user } = await getCurrentSession();
+
+ if (user === null) {
+ return redirect("/auth/login");
+ }
+
+ return (
+
+ Profile Details
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx
new file mode 100644
index 00000000..13e3853a
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ Anchor,
+ Button,
+ Group,
+ Paper,
+ PasswordInput,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+
+import { updateEmailAction } from "./actions";
+import { updateEmailSchema } from "./schema";
+
+export default function UpdateEmailForm({
+ currentEmail,
+}: {
+ currentEmail: string;
+}) {
+ const { form, handleSubmitWithAction, action } = useHookFormAction(
+ updateEmailAction,
+ zodResolver(updateEmailSchema),
+ { formProps: { defaultValues: { email: currentEmail } } }
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx
new file mode 100644
index 00000000..b22bee20
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import { showSuccessNotification } from "@/utils/notification-helpers";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ Anchor,
+ Button,
+ Group,
+ Paper,
+ PasswordInput,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+import { useState } from "react";
+
+import { updatePasswordAction } from "./actions";
+import { updatePasswordSchema } from "./schema";
+
+export default function UpdatePasswordForm() {
+ const [showForm, setShowForm] = useState(false);
+ const { form, handleSubmitWithAction, action } = useHookFormAction(
+ updatePasswordAction,
+ zodResolver(updatePasswordSchema),
+ {
+ formProps: { defaultValues: {} },
+ actionProps: {
+ onSuccess: ({ data }) => {
+ if (data?.message) {
+ showSuccessNotification(data.message);
+ setShowForm(false);
+ }
+ },
+ },
+ }
+ );
+
+ if (!showForm) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts
new file mode 100644
index 00000000..046783e4
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts
@@ -0,0 +1,19 @@
+import { z } from "zod/v4";
+
+export const updateEmailSchema = z.object({
+ email: z.string().email(),
+});
+
+export const updatePasswordSchema = z
+ .object({
+ currentPassword: z.string(),
+ newPassword: z
+ .string()
+ .min(8, { message: "Password must be at least 8 characters long" })
+ .max(255, { message: "Password is too long" }),
+ confirmNewPassword: z.string(),
+ })
+ .refine((data) => data.newPassword === data.confirmNewPassword, {
+ path: ["confirmNewPassword"],
+ message: "Passwords do not match",
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts
new file mode 100644
index 00000000..78c14d96
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts
@@ -0,0 +1,39 @@
+"use server";
+
+import {
+ createPasswordResetSession,
+ invalidateUserPasswordResetSessions,
+ sendPasswordResetEmail,
+ setPasswordResetSessionTokenCookie,
+} from "@/server/auth/utils/password-reset";
+import { generateSessionToken } from "@/server/auth/utils/session";
+import { getUserFromEmail } from "@/server/auth/utils/user";
+import { actionClient } from "@/server/safe-action";
+import { redirect } from "next/navigation";
+
+import { forgotPasswordSchema } from "./schema";
+
+export const forgotPasswordAction = actionClient
+ .schema(forgotPasswordSchema)
+ .action(async ({ parsedInput }) => {
+ const { email } = parsedInput;
+
+ const user = await getUserFromEmail(email);
+ if (user === null) {
+ return {
+ error: "Account does not exist",
+ };
+ }
+
+ await invalidateUserPasswordResetSessions(user.id);
+ const sessionToken = generateSessionToken();
+ const session = await createPasswordResetSession(
+ sessionToken,
+ user.id,
+ user.email
+ );
+
+ await sendPasswordResetEmail(session.email, session.code);
+ await setPasswordResetSessionTokenCookie(sessionToken, session.expires_at);
+ return redirect("/auth/reset-password/verify-email");
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx
new file mode 100644
index 00000000..5ff49868
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Button, Paper, Stack, Text, TextInput } from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+
+import { forgotPasswordAction } from "./actions";
+import { forgotPasswordSchema } from "./schema";
+
+export default function ForgotForm() {
+ const { form, handleSubmitWithAction, action } = useHookFormAction(
+ forgotPasswordAction,
+ zodResolver(forgotPasswordSchema),
+ {}
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx
new file mode 100644
index 00000000..09be86ba
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx
@@ -0,0 +1,22 @@
+import { Anchor, Container, Text, Title } from "@mantine/core";
+
+import ForgotForm from "./forgot-form";
+
+export default async function Page() {
+ return (
+
+ Forgot Password
+
+ Enter your email for a link to reset your password.
+
+
+
+
+
+
+ Back to login
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts
new file mode 100644
index 00000000..15829b1a
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts
@@ -0,0 +1,5 @@
+import { z } from "zod/v4";
+
+export const forgotPasswordSchema = z.object({
+ email: z.string().email(),
+});
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts
new file mode 100644
index 00000000..ca66a9df
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts
@@ -0,0 +1,35 @@
+"use server";
+
+import { getRedirectCookie } from "@/server/auth/utils/redirect";
+import {
+ createSession,
+ generateSessionToken,
+ setSessionTokenCookie,
+} from "@/server/auth/utils/session";
+import { validateLogin } from "@/server/auth/utils/user";
+import { actionClient } from "@/server/safe-action";
+import { redirect } from "next/navigation";
+
+import { loginSchema } from "./schema";
+
+export const loginAction = actionClient
+ .schema(loginSchema)
+ .action(async ({ parsedInput }) => {
+ const { email, password } = parsedInput;
+ const user = await validateLogin(email, password);
+
+ if (user === null) {
+ return { error: "Invalid email or password" };
+ }
+
+ const sessionToken = generateSessionToken();
+ const session = await createSession(sessionToken, user.id);
+ setSessionTokenCookie(sessionToken, session.expiresAt);
+
+ if (!user.emailVerified) {
+ return redirect("/auth/verify-email");
+ }
+
+ const redirectTo = await getRedirectCookie();
+ return redirect(redirectTo);
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx
new file mode 100644
index 00000000..fa13fbb5
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ Anchor,
+ Button,
+ Group,
+ Paper,
+ PasswordInput,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+
+import { loginAction } from "./actions";
+import { loginSchema } from "./schema";
+
+export default function LoginForm() {
+ const { form, handleSubmitWithAction, action } = useHookFormAction(
+ loginAction,
+ zodResolver(loginSchema),
+ {}
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx
new file mode 100644
index 00000000..a98eb6c3
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx
@@ -0,0 +1,27 @@
+import { getCurrentSession } from "@/server/auth/utils/session";
+import { Anchor, Container, Text, Title } from "@mantine/core";
+import { redirect } from "next/navigation";
+
+import LoginForm from "./login-form";
+
+export default async function Page() {
+ const { session } = await getCurrentSession();
+
+ if (session !== null) {
+ return redirect("/");
+ }
+
+ return (
+
+ Welcome back!
+
+ Do not have an account yet?{" "}
+
+ Create account
+
+
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts
new file mode 100644
index 00000000..66276d2f
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts
@@ -0,0 +1,6 @@
+import { z } from "zod/v4";
+
+export const loginSchema = z.object({
+ email: z.string().email(),
+ password: z.string(),
+});
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts
new file mode 100644
index 00000000..a781546c
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts
@@ -0,0 +1,53 @@
+"use server";
+
+import { verifyPasswordStrength } from "@/server/auth/utils/password";
+import {
+ deletePasswordResetSessionTokenCookie,
+ invalidateUserPasswordResetSessions,
+ validatePasswordResetSessionRequest,
+} from "@/server/auth/utils/password-reset";
+import {
+ createSession,
+ generateSessionToken,
+ invalidateUserSessions,
+ setSessionTokenCookie,
+} from "@/server/auth/utils/session";
+import { updateUserPassword } from "@/server/auth/utils/user";
+import { actionClient } from "@/server/safe-action";
+import { redirect } from "next/navigation";
+
+import { resetPasswordSchema } from "./schema";
+
+export const resetPasswordAction = actionClient
+ .schema(resetPasswordSchema)
+ .action(async ({ parsedInput }) => {
+ const { password } = parsedInput;
+ const { session: passwordResetSession, user } =
+ await validatePasswordResetSessionRequest();
+ if (passwordResetSession === null) {
+ return {
+ error: "Not authenticated",
+ };
+ }
+ if (!passwordResetSession.email_verified) {
+ return {
+ error: "Forbidden",
+ };
+ }
+
+ const strongPassword = await verifyPasswordStrength(password);
+ if (!strongPassword) {
+ return {
+ error: "Weak password",
+ };
+ }
+ await invalidateUserPasswordResetSessions(passwordResetSession.id_user);
+ await invalidateUserSessions(passwordResetSession.id_user);
+ await updateUserPassword(passwordResetSession.id_user, password);
+
+ const sessionToken = generateSessionToken();
+ const session = await createSession(sessionToken, user.id);
+ await setSessionTokenCookie(sessionToken, session.expiresAt);
+ await deletePasswordResetSessionTokenCookie();
+ return redirect("/");
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx
new file mode 100644
index 00000000..9a164a1d
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx
@@ -0,0 +1,33 @@
+import { env } from "@/config/env";
+import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset";
+import { Alert, Anchor, Container, Text, Title } from "@mantine/core";
+import { redirect } from "next/navigation";
+
+import ResetPasswordForm from "./reset-password-form";
+
+export default async function Page() {
+ const { session, user } = await validatePasswordResetSessionRequest();
+ if (session === null) {
+ return redirect("/auth/forgot-password");
+ }
+ if (!session.email_verified) {
+ return redirect("/auth/reset-password/verify-email");
+ }
+
+ return (
+
+ Reset Password
+
+ Enter your new password.
+
+
+
+
+
+
+ Back to login
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx
new file mode 100644
index 00000000..e11b3acd
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ Button,
+ Paper,
+ PasswordInput,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+
+import { resetPasswordAction } from "./actions";
+import { resetPasswordSchema } from "./schema";
+
+export default function ForgotForm() {
+ const { form, handleSubmitWithAction, action } = useHookFormAction(
+ resetPasswordAction,
+ zodResolver(resetPasswordSchema),
+ {}
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts
new file mode 100644
index 00000000..8315fd2c
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts
@@ -0,0 +1,14 @@
+import { z } from "zod/v4";
+
+export const resetPasswordSchema = z
+ .object({
+ password: z
+ .string()
+ .min(8, { message: "Your password should be at least 8 characters" })
+ .max(255, { message: "Password is too long" }),
+ confirmPassword: z.string(),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ path: ["confirmPassword"],
+ message: "Passwords do not match",
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts
new file mode 100644
index 00000000..4ce0b1b7
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts
@@ -0,0 +1,46 @@
+"use server";
+
+import {
+ setPasswordResetSessionAsEmailVerified,
+ validatePasswordResetSessionRequest,
+} from "@/server/auth/utils/password-reset";
+import { setUserAsEmailVerifiedIfEmailMatches } from "@/server/auth/utils/user";
+import { actionClient } from "@/server/safe-action";
+import { redirect } from "next/navigation";
+
+import { verifyEmailSchema } from "./schema";
+
+export const verifyEmailAction = actionClient
+ .schema(verifyEmailSchema)
+ .action(async ({ parsedInput }) => {
+ const { session } = await validatePasswordResetSessionRequest();
+ if (session === null) {
+ return {
+ error: "Not authenticated",
+ };
+ }
+ if (Boolean(session.email_verified)) {
+ return {
+ error: "Forbidden",
+ };
+ }
+
+ const { code } = parsedInput;
+
+ if (code !== session.code) {
+ return {
+ error: "Incorrect code",
+ };
+ }
+ await setPasswordResetSessionAsEmailVerified(session.id);
+ const emailMatches = await setUserAsEmailVerifiedIfEmailMatches(
+ session.id_user,
+ session.email
+ );
+ if (!emailMatches) {
+ return {
+ error: "Please restart the process",
+ };
+ }
+ return redirect("/auth/reset-password");
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx
new file mode 100644
index 00000000..b3796b06
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx
@@ -0,0 +1,33 @@
+import { env } from "@/config/env";
+import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset";
+import { Alert, Anchor, Container, Text, Title } from "@mantine/core";
+import { redirect } from "next/navigation";
+
+import VerifyEmailForm from "./verify-email-form";
+
+export default async function Page() {
+ const { session } = await validatePasswordResetSessionRequest();
+ if (session === null) {
+ return redirect("/auth/forgot-password");
+ }
+ if (session.email_verified) {
+ return redirect("/auth/reset-password");
+ }
+
+ return (
+
+ Verify Email
+
+ Enter the code sent to your email.
+
+
+
+
+
+
+ Back to login
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts
new file mode 100644
index 00000000..37d5311a
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts
@@ -0,0 +1,5 @@
+import { z } from "zod/v4";
+
+export const verifyEmailSchema = z.object({
+ code: z.string().length(8),
+});
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx
new file mode 100644
index 00000000..2d454b7e
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Button, Paper, PinInput, Stack, Text } from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+
+import { verifyEmailAction } from "./actions";
+import { verifyEmailSchema } from "./schema";
+
+export default function VerifyEmailForm() {
+ const { form, handleSubmitWithAction, action } = useHookFormAction(
+ verifyEmailAction,
+ zodResolver(verifyEmailSchema),
+ {}
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts
new file mode 100644
index 00000000..3faa5d0f
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts
@@ -0,0 +1,50 @@
+"use server";
+
+import {
+ createEmailVerificationRequest,
+ sendVerificationEmail,
+ setEmailVerificationRequestCookie,
+} from "@/server/auth/utils/email-verification";
+import { verifyPasswordStrength } from "@/server/auth/utils/password";
+import {
+ createSession,
+ generateSessionToken,
+ setSessionTokenCookie,
+} from "@/server/auth/utils/session";
+import { checkEmailAvailability, createUser } from "@/server/auth/utils/user";
+import { actionClient } from "@/server/safe-action";
+import { redirect } from "next/navigation";
+
+import { signupSchema } from "./schema";
+
+export const signupAction = actionClient
+ .schema(signupSchema)
+ .action(async ({ parsedInput }) => {
+ const { email, password } = parsedInput;
+ const emailAvailable = await checkEmailAvailability(email);
+ if (!emailAvailable) {
+ return { error: "Email already in use" };
+ }
+
+ const passwordStrong = await verifyPasswordStrength(password);
+ if (!passwordStrong) {
+ return { error: "Password is too weak" };
+ }
+
+ const user = await createUser(email, password);
+ const emailVerificationRequest = await createEmailVerificationRequest(
+ user.id,
+ user.email
+ );
+ await sendVerificationEmail(
+ emailVerificationRequest.email,
+ emailVerificationRequest.code
+ );
+ await setEmailVerificationRequestCookie(emailVerificationRequest);
+
+ const sessionToken = generateSessionToken();
+ const session = await createSession(sessionToken, user.id);
+ setSessionTokenCookie(sessionToken, session.expiresAt);
+
+ return redirect("/auth/verify-email");
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx
new file mode 100644
index 00000000..056d5284
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx
@@ -0,0 +1,27 @@
+import { getCurrentSession } from "@/server/auth/utils/session";
+import { Anchor, Container, Text, Title } from "@mantine/core";
+import { redirect } from "next/navigation";
+
+import SignupForm from "./signup-form";
+
+export default async function Page() {
+ const { session } = await getCurrentSession();
+
+ if (session !== null) {
+ return redirect("/");
+ }
+
+ return (
+
+ Create account
+
+ Already have an account?{" "}
+
+ Sign in
+
+
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts
new file mode 100644
index 00000000..e15638ca
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts
@@ -0,0 +1,12 @@
+import { z } from "zod/v4";
+
+export const signupSchema = z
+ .object({
+ email: z.string().email(),
+ password: z.string().min(8),
+ confirmPassword: z.string(),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ path: ["confirmPassword"],
+ message: "Passwords do not match",
+ });
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx
new file mode 100644
index 00000000..f41454ae
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ Anchor,
+ Button,
+ Group,
+ Paper,
+ PasswordInput,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+
+import { signupAction } from "./actions";
+import { signupSchema } from "./schema";
+
+export default function SignupForm() {
+ const { form, handleSubmitWithAction, action } = useHookFormAction(
+ signupAction,
+ zodResolver(signupSchema),
+ {}
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts
new file mode 100644
index 00000000..3ad9697a
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts
@@ -0,0 +1,109 @@
+"use server";
+
+import {
+ createEmailVerificationRequest,
+ deleteEmailVerificationRequestCookie,
+ deleteUserEmailVerificationRequest,
+ getUserEmailVerificationRequestFromRequest,
+ sendVerificationEmail,
+ setEmailVerificationRequestCookie,
+} from "@/server/auth/utils/email-verification";
+import { invalidateUserPasswordResetSessions } from "@/server/auth/utils/password-reset";
+import { getRedirectCookie } from "@/server/auth/utils/redirect";
+import { getCurrentSession } from "@/server/auth/utils/session";
+import { updateUserEmailAndSetEmailAsVerified } from "@/server/auth/utils/user";
+import { actionClient } from "@/server/safe-action";
+import { redirect } from "next/navigation";
+
+import { emailVerificationSchema } from "./schema";
+
+export const verifyEmailAction = actionClient
+ .schema(emailVerificationSchema)
+ .action(async ({ parsedInput, ctx }) => {
+ const { session, user } = await getCurrentSession();
+ if (session === null) {
+ return {
+ error: "Not authenticated",
+ };
+ }
+
+ let verificationRequest =
+ await getUserEmailVerificationRequestFromRequest();
+ if (verificationRequest === null) {
+ return {
+ error: "Not authenticated",
+ };
+ }
+ const { code } = parsedInput;
+ if (verificationRequest.expires_at === null) {
+ return {
+ error: "Verification code expired",
+ };
+ }
+
+ if (Date.now() >= verificationRequest.expires_at * 1000) {
+ verificationRequest = await createEmailVerificationRequest(
+ verificationRequest.id_user,
+ verificationRequest.email
+ );
+ await sendVerificationEmail(
+ verificationRequest.email,
+ verificationRequest.code
+ );
+ return {
+ error:
+ "The verification code was expired. We sent another code to your inbox.",
+ };
+ }
+ if (verificationRequest.code !== code) {
+ return {
+ error: "Incorrect code.",
+ };
+ }
+ await deleteUserEmailVerificationRequest(user.id);
+ await invalidateUserPasswordResetSessions(user.id);
+ await updateUserEmailAndSetEmailAsVerified(
+ user.id,
+ verificationRequest.email
+ );
+ await deleteEmailVerificationRequestCookie();
+
+ const redirectTo = await getRedirectCookie();
+ return redirect(redirectTo);
+ });
+
+export const resendEmailVerificationAction = actionClient.action(async () => {
+ const { session, user } = await getCurrentSession();
+ if (session === null) {
+ return {
+ error: "Not authenticated",
+ };
+ }
+
+ let verificationRequest = await getUserEmailVerificationRequestFromRequest();
+ if (verificationRequest === null) {
+ if (user.emailVerified) {
+ return {
+ error: "Forbidden",
+ };
+ }
+
+ verificationRequest = await createEmailVerificationRequest(
+ user.id,
+ user.email
+ );
+ } else {
+ verificationRequest = await createEmailVerificationRequest(
+ user.id,
+ verificationRequest.email
+ );
+ }
+ await sendVerificationEmail(
+ verificationRequest.email,
+ verificationRequest.code
+ );
+ await setEmailVerificationRequestCookie(verificationRequest);
+ return {
+ message: "A new code was sent to your inbox.",
+ };
+});
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx
new file mode 100644
index 00000000..3108c3fa
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Button, Paper, PinInput, Stack, Text } from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+
+import { verifyEmailAction } from "./actions";
+import { emailVerificationSchema } from "./schema";
+
+export default function LoginForm() {
+ const { form, handleSubmitWithAction, action } = useHookFormAction(
+ verifyEmailAction,
+ zodResolver(emailVerificationSchema),
+ {}
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx
new file mode 100644
index 00000000..bfad170d
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx
@@ -0,0 +1,40 @@
+import { getUserEmailVerificationRequestFromRequest } from "@/server/auth/utils/email-verification";
+import { getRedirectCookie } from "@/server/auth/utils/redirect";
+import { getCurrentSession } from "@/server/auth/utils/session";
+import { Anchor, Container, Text, Title } from "@mantine/core";
+import { redirect } from "next/navigation";
+
+import EmailVerificationForm from "./email-verification-form";
+import ResendButton from "./resend-button";
+
+export default async function Page() {
+ const { user } = await getCurrentSession();
+
+ if (user === null) {
+ return redirect("/auth/login");
+ }
+
+ // TODO: Ideally we'd sent a new verification email automatically if the previous one is expired,
+ // but we can't set cookies inside server components.
+ const verificationRequest =
+ await getUserEmailVerificationRequestFromRequest();
+ if (verificationRequest === null && user.emailVerified) {
+ const redirectTo = await getRedirectCookie();
+ return redirect(redirectTo);
+ }
+
+ return (
+
+ Verify your email
+
+ Enter the code sent to {verificationRequest?.email ?? user.email}
+
+
+ Change email
+
+
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx
new file mode 100644
index 00000000..ee36ae70
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { Alert, Anchor, Button, Group, Stack, Text } from "@mantine/core";
+import { useAction } from "next-safe-action/hooks";
+
+import { resendEmailVerificationAction } from "./actions";
+
+export default function ResendButton() {
+ const action = useAction(resendEmailVerificationAction);
+ return (
+
+
+
+ {"Didn't receive the email?"}
+
+
+
+
+ {action.result.data?.message && (
+ {action.result.data.message}
+ )}
+
+ {action.result.data?.error && (
+
+ {action.result.data.error}
+
+ )}
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts
new file mode 100644
index 00000000..d962f424
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts
@@ -0,0 +1,5 @@
+import { z } from "zod/v4";
+
+export const emailVerificationSchema = z.object({
+ code: z.string().length(8),
+});
diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts
new file mode 100644
index 00000000..c4e4c11f
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts
@@ -0,0 +1,19 @@
+"use server";
+
+import {
+ getCurrentSession,
+ invalidateSession,
+} from "@/server/auth/utils/session";
+import { redirect } from "next/navigation";
+
+export async function currentSessionAction() {
+ return await getCurrentSession();
+}
+
+export async function logoutAction() {
+ const { session } = await currentSessionAction();
+ if (session) {
+ await invalidateSession(session.id);
+ }
+ redirect("/");
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx
new file mode 100644
index 00000000..9bce1e21
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx
@@ -0,0 +1,18 @@
+import { getCurrentSession } from "@/server/auth/utils/session";
+
+import AuthRedirect from "./redirect";
+
+/**
+ * This server component will protect the contents of it's children from users who aren't logged in
+ * It will redirect to the login page if the user is not logged in, or the verify email page if the user is logged in but hasn't verified their email
+ */
+export default async function Protect({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const { session, user } = await getCurrentSession();
+ if (!session) return ;
+ if (!user.emailVerified) return ;
+ return <>{children}>;
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx
new file mode 100644
index 00000000..40a2afef
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import { Center, Loader } from "@mantine/core";
+import Cookies from "js-cookie";
+import { redirect } from "next/navigation";
+import { useEffect } from "react";
+
+/**
+ * A client-side component that redirects to the given path, but saves the current path in the redirectTo cookie.
+ */
+export default function AuthRedirect({ path }: { path: string }) {
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ Cookies.set("redirectTo", window.location.pathname, {
+ expires: 1 / 24 / 60, // 1 hour
+ });
+ redirect(path);
+ }
+ }, []);
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts b/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts
new file mode 100644
index 00000000..46bec5b2
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts
@@ -0,0 +1,60 @@
+import { Session } from "@/server/auth/utils/session";
+import { User } from "@/server/auth/utils/user";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import { currentSessionAction, logoutAction } from "./actions";
+
+type LogoutAction = () => Promise;
+type UseUserResult =
+ | {
+ state: "authenticated";
+ session: Session;
+ user: User;
+ logout: LogoutAction;
+ }
+ | {
+ state: "unauthenticated";
+ session: null;
+ user: null;
+ logout: LogoutAction;
+ }
+ | { state: "loading"; session: null; user: null; logout: LogoutAction };
+
+export function useUser(): UseUserResult {
+ const query = useQuery({
+ queryKey: ["current-user"],
+ queryFn: () => currentSessionAction(),
+ retry: false,
+ });
+ const queryClient = useQueryClient();
+
+ const { mutateAsync } = useMutation({
+ mutationFn: logoutAction,
+ onMutate: async () => {
+ await queryClient.cancelQueries({ queryKey: ["current-user"] });
+ queryClient.setQueryData(["current-user"], { session: null, user: null });
+ },
+ onSettled: () =>
+ queryClient.invalidateQueries({ queryKey: ["current-user"] }),
+ });
+
+ const defaultResult: UseUserResult = {
+ state: "unauthenticated",
+ session: null,
+ user: null,
+ logout: mutateAsync,
+ };
+
+ if (query.isLoading) {
+ return { ...defaultResult, state: "loading" };
+ }
+ if (query.data?.session) {
+ return {
+ ...defaultResult,
+ state: "authenticated",
+ session: query.data.session,
+ user: query.data.user,
+ };
+ }
+ return defaultResult;
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx
new file mode 100644
index 00000000..e4fd0778
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import { Button, Menu, px, Skeleton } from "@mantine/core";
+import { IconChevronDown, IconLogout, IconUser } from "@tabler/icons-react";
+import Link from "next/link";
+
+import { useUser } from "./use-user";
+
+export default function UserMenu() {
+ const { state, session, user, logout } = useUser();
+
+ if (state === "loading") {
+ return ;
+ }
+ if (state === "unauthenticated") {
+ return (
+
+ );
+ }
+ return (
+
+ );
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx b/packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx
new file mode 100644
index 00000000..e09789c5
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx
@@ -0,0 +1,137 @@
+import { Body, Container, Head, Heading, Html, Img, Section, Text } from "@react-email/components";
+
+interface AuthCodeEmailProps {
+ validationCode: string;
+ type: "verification" | "password-reset";
+}
+
+export const AuthCodeEmail = ({ validationCode, type }: AuthCodeEmailProps) => (
+
+
+
+
+
+ {type === "verification" ? "Verify Your Email" : "Reset Your Password"}
+
+ Enter the following code to {type === "verification" ? "verify your email" : "reset your password"}
+
+
+ If you did not request this code, you can ignore this email.
+
+
+
+);
+
+AuthCodeEmail.PreviewProps = {
+ validationCode: "D7CU4GOV",
+ type: "verification",
+} as AuthCodeEmailProps;
+
+export default AuthCodeEmail;
+
+const main = {
+ backgroundColor: "#ffffff",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+};
+
+const container = {
+ backgroundColor: "#ffffff",
+ border: "1px solid #eee",
+ borderRadius: "5px",
+ boxShadow: "0 5px 10px rgba(20,50,70,.2)",
+ marginTop: "20px",
+ maxWidth: "360px",
+ margin: "0 auto",
+ padding: "68px 0 130px",
+};
+
+const logo: React.CSSProperties = {
+ margin: "0 auto",
+};
+
+const tertiary = {
+ color: "#0a85ea",
+ fontSize: "11px",
+ fontWeight: 700,
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ height: "16px",
+ letterSpacing: "0",
+ lineHeight: "16px",
+ margin: "16px 8px 8px 8px",
+ textTransform: "uppercase" as const,
+ textAlign: "center" as const,
+};
+
+const secondary = {
+ color: "#000",
+ display: "inline-block",
+ fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif",
+ fontSize: "20px",
+ fontWeight: 500,
+ lineHeight: "24px",
+ marginBottom: "0",
+ marginTop: "0",
+ textAlign: "center" as const,
+ padding: "0 40px",
+};
+
+const codeContainer = {
+ background: "rgba(0,0,0,.05)",
+ borderRadius: "4px",
+ margin: "16px auto 14px",
+ verticalAlign: "middle",
+ width: "280px",
+};
+
+const code = {
+ color: "#000",
+ display: "inline-block",
+ fontFamily: "HelveticaNeue-Bold",
+ fontSize: "32px",
+ fontWeight: 700,
+ letterSpacing: "6px",
+ lineHeight: "40px",
+ paddingBottom: "8px",
+ paddingTop: "8px",
+ margin: "0 auto",
+ width: "100%",
+ textAlign: "center" as const,
+};
+
+const paragraph = {
+ color: "#444",
+ fontSize: "15px",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ letterSpacing: "0",
+ lineHeight: "23px",
+ padding: "0 40px",
+ margin: "0",
+ textAlign: "center" as const,
+};
+
+const link = {
+ color: "#444",
+ textDecoration: "underline",
+};
+
+const footer = {
+ color: "#000",
+ fontSize: "12px",
+ fontWeight: 800,
+ letterSpacing: "0",
+ lineHeight: "23px",
+ margin: "0",
+ marginTop: "20px",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ textAlign: "center" as const,
+ textTransform: "uppercase" as const,
+};
diff --git a/packages/cli-old/template/extras/fmaddon-auth/middleware.ts b/packages/cli-old/template/extras/fmaddon-auth/middleware.ts
new file mode 100644
index 00000000..86ea06f7
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/middleware.ts
@@ -0,0 +1,44 @@
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+
+export async function middleware(request: NextRequest): Promise {
+ if (request.method === "GET") {
+ const response = NextResponse.next();
+ const token = request.cookies.get("session")?.value ?? null;
+ if (token !== null) {
+ // Only extend cookie expiration on GET requests since we can be sure
+ // a new session wasn't set when handling the request.
+ response.cookies.set("session", token, {
+ path: "/",
+ maxAge: 60 * 60 * 24 * 30,
+ sameSite: "lax",
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ });
+ }
+ return response;
+ }
+
+ const originHeader = request.headers.get("Origin");
+ // NOTE: You may need to use `X-Forwarded-Host` instead
+ const hostHeader = request.headers.get("Host");
+ if (originHeader === null || hostHeader === null) {
+ return new NextResponse(null, {
+ status: 403,
+ });
+ }
+ let origin: URL;
+ try {
+ origin = new URL(originHeader);
+ } catch {
+ return new NextResponse(null, {
+ status: 403,
+ });
+ }
+ if (origin.host !== hostHeader) {
+ return new NextResponse(null, {
+ status: 403,
+ });
+ }
+ return NextResponse.next();
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts
new file mode 100644
index 00000000..253eab74
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts
@@ -0,0 +1,137 @@
+import { encodeBase32 } from "@oslojs/encoding";
+import { cookies } from "next/headers";
+
+import { emailVerificationLayout } from "../db/client";
+import { TemailVerification } from "../db/emailVerification";
+import { sendEmail } from "../email";
+import { generateRandomOTP } from "./index";
+import { getCurrentSession } from "./session";
+
+/**
+ * An Email Verification Request is a record in the email verification table that is created when a user requests to change their email address. It's like a temporary session which can expire if the user doesn't verify the new email address within a certain amount of time.
+ */
+
+/**
+ * Get a user's email verification request.
+ * @param userId - The ID of the user.
+ * @param id - The ID of the email verification request.
+ * @returns The email verification request, or null if it doesn't exist.
+ */
+export async function getUserEmailVerificationRequest(
+ userId: string,
+ id: string
+): Promise {
+ const result = await emailVerificationLayout.maybeFindFirst({
+ query: { id_user: `==${userId}`, id: `==${id}` },
+ });
+ return result?.data.fieldData ?? null;
+}
+
+/**
+ * Create a new email verification request for a user.
+ * @param id_user - The ID of the user.
+ * @param email - The email address to verify.
+ * @returns The email verification request.
+ */
+export async function createEmailVerificationRequest(
+ id_user: string,
+ email: string
+): Promise {
+ deleteUserEmailVerificationRequest(id_user);
+ const idBytes = new Uint8Array(20);
+ crypto.getRandomValues(idBytes);
+ const id = encodeBase32(idBytes).toLowerCase();
+
+ const code = generateRandomOTP();
+ const expiresAt = new Date(Date.now() + 1000 * 60 * 10);
+
+ const request: TemailVerification = {
+ id,
+ code,
+ expires_at: Math.floor(expiresAt.getTime() / 1000),
+ email,
+ id_user,
+ };
+
+ await emailVerificationLayout.create({
+ fieldData: request,
+ });
+
+ return request;
+}
+
+/**
+ * Delete a user's email verification request.
+ * @param id_user - The ID of the user.
+ */
+export async function deleteUserEmailVerificationRequest(
+ id_user: string
+): Promise {
+ const result = await emailVerificationLayout.maybeFindFirst({
+ query: { id_user: `==${id_user}` },
+ });
+ if (result === null) return;
+
+ await emailVerificationLayout.delete({ recordId: result.data.recordId });
+}
+
+/**
+ * Send a verification email to a user.
+ * @param email - The email address to send the verification email to.
+ * @param code - The verification code to send to the user.
+ */
+export async function sendVerificationEmail(
+ email: string,
+ code: string
+): Promise {
+ await sendEmail({ to: email, code, type: "verification" });
+}
+
+/**
+ * Set a cookie for a user's email verification request.
+ * @param request - The email verification request.
+ */
+export async function setEmailVerificationRequestCookie(
+ request: TemailVerification
+): Promise {
+ (await cookies()).set("email_verification", request.id, {
+ httpOnly: true,
+ path: "/",
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ expires: request.expires_at
+ ? new Date(request.expires_at * 1000)
+ : new Date(Date.now() + 1000 * 60 * 60),
+ });
+}
+
+/**
+ * Delete the cookie for a user's email verification request.
+ */
+export async function deleteEmailVerificationRequestCookie(): Promise {
+ (await cookies()).set("email_verification", "", {
+ httpOnly: true,
+ path: "/",
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 0,
+ });
+}
+
+/**
+ * Get a user's email verification request from the cookie.
+ * @returns The email verification request, or null if it doesn't exist.
+ */
+export async function getUserEmailVerificationRequestFromRequest(): Promise {
+ const { user } = await getCurrentSession();
+ if (user === null) {
+ return null;
+ }
+ const id = (await cookies()).get("email_verification")?.value ?? null;
+ if (id === null) {
+ return null;
+ }
+ const request = await getUserEmailVerificationRequest(user.id, id);
+
+ return request;
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts
new file mode 100644
index 00000000..377f773d
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts
@@ -0,0 +1,51 @@
+import { createCipheriv, createDecipheriv } from "crypto";
+import { DynamicBuffer } from "@oslojs/binary";
+import { decodeBase64 } from "@oslojs/encoding";
+
+const key = decodeBase64(process.env.ENCRYPTION_KEY ?? "");
+
+export function encrypt(data: Uint8Array): Uint8Array {
+ const iv = new Uint8Array(16);
+ crypto.getRandomValues(iv);
+ const cipher = createCipheriv("aes-128-gcm", key, iv);
+ const encrypted = new DynamicBuffer(0);
+ encrypted.write(iv);
+ encrypted.write(cipher.update(data));
+ encrypted.write(cipher.final());
+ encrypted.write(cipher.getAuthTag());
+ return encrypted.bytes();
+}
+
+/**
+ * Encrypt a string for storage in the database.
+ * Here we're returning a base64 encoded string since FileMaker doesn't store binary data.
+ * @param data - The string to encrypt.
+ * @returns The encrypted string.
+ */
+export function encryptString(data: string): string {
+ const encrypted = encrypt(new TextEncoder().encode(data));
+ return Buffer.from(encrypted).toString("base64");
+}
+
+/**
+ * Decrypt a string stored in the database.
+ * @param encrypted - The encrypted string to decrypt.
+ * @returns The decrypted string.
+ */
+export function decrypt(encrypted: Uint8Array): Uint8Array {
+ if (encrypted.byteLength < 33) {
+ throw new Error("Invalid data");
+ }
+ const decipher = createDecipheriv("aes-128-gcm", key, encrypted.slice(0, 16));
+ decipher.setAuthTag(encrypted.slice(encrypted.byteLength - 16));
+ const decrypted = new DynamicBuffer(0);
+ decrypted.write(
+ decipher.update(encrypted.slice(16, encrypted.byteLength - 16))
+ );
+ decrypted.write(decipher.final());
+ return decrypted.bytes();
+}
+
+export function decryptToString(data: Uint8Array): string {
+ return new TextDecoder().decode(decrypt(data));
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts
new file mode 100644
index 00000000..41849aef
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts
@@ -0,0 +1,16 @@
+import { encodeBase32UpperCaseNoPadding } from "@oslojs/encoding";
+
+export function generateRandomOTP(): string {
+ const bytes = new Uint8Array(5);
+ crypto.getRandomValues(bytes);
+ const code = encodeBase32UpperCaseNoPadding(bytes);
+ return code;
+}
+
+export const options = {
+ password: {
+ minLength: 8,
+ maxLength: 255,
+ checkCompromised: false, // set to true to prevent known compromised passwords on signup
+ },
+};
diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts
new file mode 100644
index 00000000..d3434460
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts
@@ -0,0 +1,153 @@
+import { sha256 } from "@oslojs/crypto/sha2";
+import { encodeHexLowerCase } from "@oslojs/encoding";
+import { cookies } from "next/headers";
+
+import { passwordResetLayout } from "../db/client";
+import { TpasswordReset } from "../db/passwordReset";
+import { sendEmail } from "../email";
+import { generateRandomOTP } from "./index";
+import type { User } from "./user";
+
+type PasswordResetSession = Omit<
+ TpasswordReset,
+ | "proofkit_auth_users::email"
+ | "proofkit_auth_users::emailVerified"
+ | "proofkit_auth_users::username"
+>;
+
+export async function createPasswordResetSession(
+ token: string,
+ id_user: string,
+ email: string
+): Promise {
+ const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
+ const session: PasswordResetSession = {
+ id: sessionId,
+ id_user,
+ email,
+ expires_at: Math.floor(
+ new Date(Date.now() + 1000 * 60 * 10).getTime() / 1000
+ ),
+ code: generateRandomOTP(),
+ email_verified: 0,
+ };
+ await passwordResetLayout.create({ fieldData: session });
+
+ return session;
+}
+
+/**
+ * Validate a password reset session token.
+ * @param token - The password reset session token.
+ * @returns The password reset session, or null if it doesn't exist.
+ */
+export async function validatePasswordResetSessionToken(
+ token: string
+): Promise {
+ const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
+ const row = await passwordResetLayout.maybeFindFirst({
+ query: { id: `==${sessionId}` },
+ });
+
+ if (row === null) {
+ return { session: null, user: null };
+ }
+ const session: PasswordResetSession = {
+ id: row.data.fieldData.id,
+ id_user: row.data.fieldData.id_user,
+ email: row.data.fieldData.email,
+ code: row.data.fieldData.code,
+ expires_at: row.data.fieldData.expires_at,
+ email_verified: row.data.fieldData.email_verified,
+ };
+
+ const user: User = {
+ id: row.data.fieldData.id_user,
+ email: row.data.fieldData["proofkit_auth_users::email"],
+ username: row.data.fieldData["proofkit_auth_users::username"],
+ emailVerified: Boolean(
+ row.data.fieldData["proofkit_auth_users::emailVerified"]
+ ),
+ };
+ if (session.expires_at && Date.now() >= session.expires_at * 1000) {
+ await passwordResetLayout.delete({ recordId: row.data.recordId });
+ return { session: null, user: null };
+ }
+ return { session, user };
+}
+
+async function fetchPasswordResetSession(sessionId: string) {
+ return (
+ await passwordResetLayout.findOne({ query: { id: `==${sessionId}` } })
+ ).data;
+}
+
+export async function setPasswordResetSessionAsEmailVerified(
+ sessionId: string
+): Promise {
+ const { recordId } = await fetchPasswordResetSession(sessionId);
+ await passwordResetLayout.update({
+ recordId,
+ fieldData: { email_verified: 1 },
+ });
+}
+
+export async function invalidateUserPasswordResetSessions(
+ userId: string
+): Promise {
+ const sessions = await passwordResetLayout.find({
+ query: { id_user: `==${userId}` },
+ ignoreEmptyResult: true,
+ });
+ for (const session of sessions.data) {
+ await passwordResetLayout.delete({ recordId: session.recordId });
+ }
+}
+
+export async function validatePasswordResetSessionRequest(): Promise {
+ const token = (await cookies()).get("password_reset_session")?.value ?? null;
+ if (token === null) {
+ return { session: null, user: null };
+ }
+ const result = await validatePasswordResetSessionToken(token);
+ if (result.session === null) {
+ deletePasswordResetSessionTokenCookie();
+ }
+ return result;
+}
+
+export async function setPasswordResetSessionTokenCookie(
+ token: string,
+ expiresAt: number | null
+): Promise {
+ (await cookies()).set("password_reset_session", token, {
+ expires: expiresAt
+ ? new Date(expiresAt * 1000)
+ : new Date(Date.now() + 60 * 60 * 1000),
+ sameSite: "lax",
+ httpOnly: true,
+ path: "/",
+ secure: process.env.NODE_ENV === "production",
+ });
+}
+
+export async function deletePasswordResetSessionTokenCookie(): Promise {
+ (await cookies()).set("password_reset_session", "", {
+ maxAge: 0,
+ sameSite: "lax",
+ httpOnly: true,
+ path: "/",
+ secure: process.env.NODE_ENV === "production",
+ });
+}
+
+export async function sendPasswordResetEmail(
+ email: string,
+ code: string
+): Promise {
+ await sendEmail({ to: email, code, type: "password-reset" });
+}
+
+export type PasswordResetSessionValidationResult =
+ | { session: PasswordResetSession; user: User }
+ | { session: null; user: null };
diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts
new file mode 100644
index 00000000..bf723a6f
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts
@@ -0,0 +1,67 @@
+import { options } from ".";
+import { hash, verify } from "@node-rs/argon2";
+import { sha1 } from "@oslojs/crypto/sha1";
+import { encodeHexLowerCase } from "@oslojs/encoding";
+
+/**
+ * Hash a password using Argon2.
+ * @param password - The password to hash.
+ * @returns The hashed password.
+ */
+export async function hashPassword(password: string): Promise {
+ return await hash(password, {
+ memoryCost: 19456,
+ timeCost: 2,
+ outputLen: 32,
+ parallelism: 1,
+ });
+}
+
+/**
+ * Verify that a password matches a hash.
+ * @param hash - The hash to verify against.
+ * @param password - The password to verify.
+ * @returns True if the password matches the hash, false otherwise.
+ */
+export async function verifyPasswordHash(
+ hash: string,
+ password: string
+): Promise {
+ return await verify(hash, password);
+}
+
+/**
+ * Verify that a password is strong enough.
+ * @param password - The password to verify.
+ * @returns True if the password is strong enough, false otherwise.
+ */
+export async function verifyPasswordStrength(
+ password: string
+): Promise {
+ if (
+ password.length < options.password.minLength ||
+ password.length > options.password.maxLength
+ ) {
+ return false;
+ }
+
+ if (options.password.checkCompromised) {
+ const hash = encodeHexLowerCase(sha1(new TextEncoder().encode(password)));
+ const hashPrefix = hash.slice(0, 5);
+ const response = await fetch(
+ `https://api.pwnedpasswords.com/range/${hashPrefix}`
+ );
+ const data = await response.text();
+ const items = data.split("\n");
+ for (const item of items) {
+ const hashSuffix = item.slice(0, 35).toLowerCase();
+ if (hash === hashPrefix + hashSuffix) {
+ console.log(
+ "User's new password was found in list of compromised passwords, reject"
+ );
+ return false;
+ }
+ }
+ }
+ return true;
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts
new file mode 100644
index 00000000..eb3b467b
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts
@@ -0,0 +1,8 @@
+import { cookies } from "next/headers";
+
+export async function getRedirectCookie() {
+ const cookieStore = await cookies();
+ const redirectTo = cookieStore.get("redirectTo")?.value;
+ cookieStore.delete("redirectTo");
+ return redirectTo ?? "/";
+}
diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts
new file mode 100644
index 00000000..aaa80f35
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts
@@ -0,0 +1,191 @@
+import { sha256 } from "@oslojs/crypto/sha2";
+import {
+ encodeBase32LowerCaseNoPadding,
+ encodeHexLowerCase,
+} from "@oslojs/encoding";
+import { cookies } from "next/headers";
+import { cache } from "react";
+
+import { sessionsLayout } from "../db/client";
+import { Tsessions as _Session } from "../db/sessions";
+import type { User } from "./user";
+
+/**
+ * Generate a random session token with sufficient entropy for a session ID.
+ * @returns The session token.
+ */
+export function generateSessionToken(): string {
+ const bytes = new Uint8Array(20);
+ crypto.getRandomValues(bytes);
+ const token = encodeBase32LowerCaseNoPadding(bytes);
+ return token;
+}
+
+/**
+ * Create a new session for a user and save it to the database.
+ * @param token - The session token.
+ * @param userId - The ID of the user.
+ * @returns The session.
+ */
+export async function createSession(
+ token: string,
+ userId: string
+): Promise {
+ const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
+ const session: Session = {
+ id: sessionId,
+ id_user: userId,
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
+ };
+
+ // create session in DB
+ await sessionsLayout.create({
+ fieldData: {
+ id: session.id,
+ id_user: session.id_user,
+ expiresAt: Math.floor(session.expiresAt.getTime() / 1000),
+ },
+ });
+
+ return session;
+}
+
+/**
+ * Invalidate a session by deleting it from the database.
+ * @param sessionId - The ID of the session to invalidate.
+ */
+export async function invalidateSession(sessionId: string): Promise {
+ const fmResult = await sessionsLayout.maybeFindFirst({
+ query: { id: `==${sessionId}` },
+ });
+ if (fmResult === null) {
+ return;
+ }
+ await sessionsLayout.delete({ recordId: fmResult.data.recordId });
+}
+
+/**
+ * Validate a session token to make sure it still exists in the database and hasn't expired.
+ * @param token - The session token.
+ * @returns The session, or null if it doesn't exist.
+ */
+export async function validateSessionToken(
+ token: string
+): Promise {
+ const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
+
+ const result = await sessionsLayout.maybeFindFirst({
+ query: { id: `==${sessionId}` },
+ });
+ if (result === null) {
+ return { session: null, user: null };
+ }
+
+ const fmResult = result.data.fieldData;
+ const recordId = result.data.recordId;
+ const session: Session = {
+ id: fmResult.id,
+ id_user: fmResult.id_user,
+ expiresAt: fmResult.expiresAt
+ ? new Date(fmResult.expiresAt * 1000)
+ : new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
+ };
+
+ const user: User = {
+ id: session.id_user,
+ email: fmResult["proofkit_auth_users::email"],
+ emailVerified: Boolean(fmResult["proofkit_auth_users::emailVerified"]),
+ username: fmResult["proofkit_auth_users::username"],
+ };
+
+ // delete session if it has expired
+ if (Date.now() >= session.expiresAt.getTime()) {
+ await sessionsLayout.delete({ recordId });
+ return { session: null, user: null };
+ }
+
+ // extend session if it's going to expire soon
+ // You may want to customize this logic to better suit your app's requirements
+ if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
+ session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
+ await sessionsLayout.update({
+ recordId,
+ fieldData: {
+ expiresAt: Math.floor(session.expiresAt.getTime() / 1000),
+ },
+ });
+ }
+
+ return { session, user };
+}
+
+/**
+ * Get the current session from the cookie.
+ * Wrapped in a React cache to avoid calling the database more than once per request
+ * This function can be used in server components, server actions, and route handlers (but importantly not middleware).
+ * @returns The session, or null if it doesn't exist.
+ */
+export const getCurrentSession = cache(
+ async (): Promise => {
+ const token = (await cookies()).get("session")?.value ?? null;
+ if (token === null) {
+ return { session: null, user: null };
+ }
+ const result = await validateSessionToken(token);
+ return result;
+ }
+);
+
+/**
+ * Invalidate all sessions for a user by deleting them from the database.
+ * @param userId - The ID of the user.
+ */
+export async function invalidateUserSessions(userId: string): Promise {
+ const sessions = await sessionsLayout.findAll({
+ query: { id_user: `==${userId}` },
+ });
+ for (const session of sessions) {
+ await sessionsLayout.delete({ recordId: session.recordId });
+ }
+}
+
+/**
+ * Set a cookie for a session.
+ * @param token - The session token.
+ * @param expiresAt - The expiration date of the session.
+ */
+export async function setSessionTokenCookie(
+ token: string,
+ expiresAt: Date
+): Promise {
+ (await cookies()).set("session", token, {
+ httpOnly: true,
+ path: "/",
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ expires: expiresAt,
+ });
+}
+
+/**
+ * Delete the session cookie.
+ */
+export async function deleteSessionTokenCookie(): Promise {
+ (await cookies()).set("session", "", {
+ httpOnly: true,
+ path: "/",
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 0,
+ });
+}
+
+export interface Session {
+ id: string;
+ expiresAt: Date;
+ id_user: string;
+}
+
+type SessionValidationResult =
+ | { session: Session; user: User }
+ | { session: null; user: null };
diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts
new file mode 100644
index 00000000..1b7e0194
--- /dev/null
+++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts
@@ -0,0 +1,146 @@
+import { usersLayout } from "../db/client";
+import { Tusers as _User } from "../db/users";
+import { hashPassword, verifyPasswordHash } from "./password";
+
+export type User = Partial<
+ Omit<_User, "id" | "password_hash" | "recovery_code" | "emailVerified">
+> & {
+ id: string;
+ email: string;
+ emailVerified: boolean;
+};
+
+/** An internal helper function to fetch a user from the database. */
+async function fetchUser(userId: string) {
+ const { data } = await usersLayout.findOne({
+ query: { id: `==${userId}` },
+ });
+ return data;
+}
+
+/** Create a new user in the database. */
+export async function createUser(
+ email: string,
+ password: string
+): Promise {
+ const password_hash = await hashPassword(password);
+ const { recordId } = await usersLayout.create({
+ fieldData: {
+ email,
+ password_hash,
+ emailVerified: 0,
+ },
+ });
+ const fmResult = await usersLayout.get({ recordId });
+ const { fieldData } = fmResult.data[0];
+
+ const user: User = {
+ id: fieldData.id,
+ email,
+ emailVerified: false,
+ username: "",
+ };
+ return user;
+}
+
+/** Update a user's password in the database. */
+export async function updateUserPassword(
+ userId: string,
+ password: string
+): Promise {
+ const password_hash = await hashPassword(password);
+ const { recordId } = await fetchUser(userId);
+
+ await usersLayout.update({ recordId, fieldData: { password_hash } });
+}
+
+export async function updateUserEmailAndSetEmailAsVerified(
+ userId: string,
+ email: string
+): Promise {
+ const { recordId } = await fetchUser(userId);
+ await usersLayout.update({
+ recordId,
+ fieldData: { email, emailVerified: 1 },
+ });
+}
+
+export async function setUserAsEmailVerifiedIfEmailMatches(
+ userId: string,
+ email: string
+): Promise {
+ try {
+ const {
+ data: { recordId },
+ } = await usersLayout.findOne({
+ query: { id: `==${userId}`, email: `==${email}` },
+ });
+ await usersLayout.update({ recordId, fieldData: { emailVerified: 1 } });
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
+
+export async function getUserFromEmail(email: string): Promise {
+ const fmResult = await usersLayout.maybeFindFirst({
+ query: { email: `==${email}` },
+ });
+ if (fmResult === null) return null;
+
+ const {
+ data: { fieldData },
+ } = fmResult;
+
+ const user: User = {
+ id: fieldData.id,
+ email: fieldData.email,
+ emailVerified: Boolean(fieldData.emailVerified),
+ username: fieldData.username,
+ };
+ return user;
+}
+
+/**
+ * Validate a user's email/password combination.
+ * @param email - The user's email.
+ * @param password - The user's password.
+ * @returns The user, or null if the login is invalid.
+ */
+export async function validateLogin(
+ email: string,
+ password: string
+): Promise {
+ try {
+ const {
+ data: { fieldData },
+ } = await usersLayout.findOne({
+ query: { email: `==${email}` },
+ });
+
+ const validPassword = await verifyPasswordHash(
+ fieldData.password_hash,
+ password
+ );
+ if (!validPassword) {
+ return null;
+ }
+ const user: User = {
+ id: fieldData.id,
+ email: fieldData.email,
+ emailVerified: Boolean(fieldData.emailVerified),
+ username: fieldData.username,
+ };
+ return user;
+ } catch (error) {
+ return null;
+ }
+}
+
+export async function checkEmailAvailability(email: string): Promise {
+ const { data } = await usersLayout.find({
+ query: { email: `==${email}` },
+ ignoreEmptyResult: true,
+ });
+ return data.length === 0;
+}
diff --git a/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma b/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma
new file mode 100644
index 00000000..6b9dd139
--- /dev/null
+++ b/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma
@@ -0,0 +1,24 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+ previewFeatures = ["driverAdapters"]
+}
+
+datasource db {
+ provider = "mysql"
+ url = env("DATABASE_URL")
+
+ // If you have enabled foreign key constraints for your database, remove this line.
+ relationMode = "prisma"
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@index([name])
+}
diff --git a/packages/cli-old/template/extras/prisma/schema/base.prisma b/packages/cli-old/template/extras/prisma/schema/base.prisma
new file mode 100644
index 00000000..ddb6e099
--- /dev/null
+++ b/packages/cli-old/template/extras/prisma/schema/base.prisma
@@ -0,0 +1,20 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@index([name])
+}
diff --git a/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma b/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma
new file mode 100644
index 00000000..198915b9
--- /dev/null
+++ b/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma
@@ -0,0 +1,77 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+ previewFeatures = ["driverAdapters"]
+}
+
+datasource db {
+ provider = "mysql"
+ url = env("DATABASE_URL")
+
+ // If you have enabled foreign key constraints for your database, remove this line.
+ relationMode = "prisma"
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ createdBy User @relation(fields: [createdById], references: [id])
+ createdById String
+
+ @@index([name])
+ @@index([createdById])
+}
+
+// Necessary for Next auth
+model Account {
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String? @db.Text
+ access_token String? @db.Text
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String? @db.Text
+ session_state String?
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([provider, providerAccountId])
+ @@index([userId])
+}
+
+model Session {
+ id String @id @default(cuid())
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@index([userId])
+}
+
+model User {
+ id String @id @default(cuid())
+ name String?
+ email String? @unique
+ emailVerified DateTime?
+ image String?
+ accounts Account[]
+ sessions Session[]
+ posts Post[]
+}
+
+model VerificationToken {
+ identifier String
+ token String @unique
+ expires DateTime
+
+ @@unique([identifier, token])
+}
diff --git a/packages/cli-old/template/extras/prisma/schema/with-auth.prisma b/packages/cli-old/template/extras/prisma/schema/with-auth.prisma
new file mode 100644
index 00000000..b17831e6
--- /dev/null
+++ b/packages/cli-old/template/extras/prisma/schema/with-auth.prisma
@@ -0,0 +1,74 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
+ // Further reading:
+ // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
+ // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
+ url = env("DATABASE_URL")
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ createdBy User @relation(fields: [createdById], references: [id])
+ createdById String
+
+ @@index([name])
+}
+
+// Necessary for Next auth
+model Account {
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String? // @db.Text
+ access_token String? // @db.Text
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String? // @db.Text
+ session_state String?
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ refresh_token_expires_in Int?
+
+ @@unique([provider, providerAccountId])
+}
+
+model Session {
+ id String @id @default(cuid())
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+model User {
+ id String @id @default(cuid())
+ name String?
+ email String? @unique
+ emailVerified DateTime?
+ image String?
+ accounts Account[]
+ sessions Session[]
+ posts Post[]
+}
+
+model VerificationToken {
+ identifier String
+ token String @unique
+ expires DateTime
+
+ @@unique([identifier, token])
+}
diff --git a/packages/cli-old/template/extras/src/app/_components/post-tw.tsx b/packages/cli-old/template/extras/src/app/_components/post-tw.tsx
new file mode 100644
index 00000000..ebe15eab
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/_components/post-tw.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useState } from "react";
+
+import { api } from "~/trpc/react";
+
+export function LatestPost() {
+ const [latestPost] = api.post.getLatest.useSuspenseQuery();
+
+ const utils = api.useUtils();
+ const [name, setName] = useState("");
+ const createPost = api.post.create.useMutation({
+ onSuccess: async () => {
+ await utils.post.invalidate();
+ setName("");
+ },
+ });
+
+ return (
+
+ {latestPost ? (
+
Your most recent post: {latestPost.name}
+ ) : (
+
You have no posts yet.
+ )}
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/_components/post.tsx b/packages/cli-old/template/extras/src/app/_components/post.tsx
new file mode 100644
index 00000000..1ad81347
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/_components/post.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { useState } from "react";
+
+import { api } from "~/trpc/react";
+import styles from "../index.module.css";
+
+export function LatestPost() {
+ const [latestPost] = api.post.getLatest.useSuspenseQuery();
+
+ const utils = api.useUtils();
+ const [name, setName] = useState("");
+ const createPost = api.post.create.useMutation({
+ onSuccess: async () => {
+ await utils.post.invalidate();
+ setName("");
+ },
+ });
+
+ return (
+
+ {latestPost ? (
+
+ Your most recent post: {latestPost.name}
+
+ ) : (
+
You have no posts yet.
+ )}
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/api/auth/[...nextauth]/route.ts b/packages/cli-old/template/extras/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 00000000..fbb80152
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,4 @@
+import { handlers } from "@/server/auth"; // Referring to the auth.ts we just created
+
+
+export const { GET, POST } = handlers;
diff --git a/packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts b/packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts
new file mode 100644
index 00000000..5fbd827d
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts
@@ -0,0 +1,34 @@
+import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
+import { type NextRequest } from "next/server";
+
+import { env } from "~/env";
+import { appRouter } from "~/server/api/root";
+import { createTRPCContext } from "~/server/api/trpc";
+
+/**
+ * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
+ * handling a HTTP request (e.g. when you make requests from Client Components).
+ */
+const createContext = async (req: NextRequest) => {
+ return createTRPCContext({
+ headers: req.headers,
+ });
+};
+
+const handler = (req: NextRequest) =>
+ fetchRequestHandler({
+ endpoint: "/api/trpc",
+ req,
+ router: appRouter,
+ createContext: () => createContext(req),
+ onError:
+ env.NODE_ENV === "development"
+ ? ({ path, error }) => {
+ console.error(
+ `❌ tRPC failed on ${path ?? ""}: ${error.message}`
+ );
+ }
+ : undefined,
+ });
+
+export { handler as GET, handler as POST };
diff --git a/packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx b/packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx
new file mode 100644
index 00000000..4382acb8
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx
@@ -0,0 +1,10 @@
+import { Center } from "@mantine/core";
+import React from "react";
+
+export default function AuthLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return {children};
+}
diff --git a/packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx b/packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx
new file mode 100644
index 00000000..2cc13d4c
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx
@@ -0,0 +1,5 @@
+import { SignIn } from "@clerk/nextjs";
+
+export default function Page() {
+ return ;
+}
diff --git a/packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx b/packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx
new file mode 100644
index 00000000..27439454
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx
@@ -0,0 +1,5 @@
+import { SignUp } from "@clerk/nextjs";
+
+export default function Page() {
+ return ;
+}
diff --git a/packages/cli-old/template/extras/src/app/layout/base.tsx b/packages/cli-old/template/extras/src/app/layout/base.tsx
new file mode 100644
index 00000000..e0382db7
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/layout/base.tsx
@@ -0,0 +1,34 @@
+import { ColorSchemeScript, MantineProvider } from "@mantine/core";
+import { ModalsProvider } from "@mantine/modals";
+import { Notifications } from "@mantine/notifications";
+
+import "@mantine/core/styles.css";
+import "@mantine/notifications/styles.css";
+import "@mantine/dates/styles.css";
+import "mantine-react-table/styles.css";
+
+import { type Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "My ProofKit App",
+ description: "Generated by proofkit",
+ icons: [{ rel: "icon", url: "/favicon.ico" }],
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{ children: React.ReactNode }>) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/layout/main-shell.tsx b/packages/cli-old/template/extras/src/app/layout/main-shell.tsx
new file mode 100644
index 00000000..77fa7adf
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/layout/main-shell.tsx
@@ -0,0 +1,37 @@
+import {
+ AppShell,
+ AppShellFooter,
+ AppShellHeader,
+ AppShellMain,
+ AppShellNavbar,
+} from "@mantine/core";
+import React from "react";
+
+/** Layout configuration Edit these values to change the layout */
+export const showHeader = false;
+export const showFooter = false;
+export const showLeftNavbar = false;
+
+export const headerHeight = 60;
+export const footerHeight = 60;
+export const leftNavbarWidth = 200;
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {showHeader && Header}
+ {showLeftNavbar && Left Navbar}
+ {children}
+ {showFooter && Footer}
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx b/packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx
new file mode 100644
index 00000000..c1218810
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx
@@ -0,0 +1,24 @@
+import "~/styles/globals.css";
+
+import { GeistSans } from "geist/font/sans";
+import { type Metadata } from "next";
+
+import { TRPCReactProvider } from "~/trpc/react";
+
+export const metadata: Metadata = {
+ title: "Create T3 App",
+ description: "Generated by proofkit",
+ icons: [{ rel: "icon", url: "/favicon.ico" }],
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{ children: React.ReactNode }>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/layout/with-trpc.tsx b/packages/cli-old/template/extras/src/app/layout/with-trpc.tsx
new file mode 100644
index 00000000..6471a2ae
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/layout/with-trpc.tsx
@@ -0,0 +1,24 @@
+import "~/styles/globals.css";
+
+import { GeistSans } from "geist/font/sans";
+import { type Metadata } from "next";
+
+import { TRPCReactProvider } from "~/trpc/react";
+
+export const metadata: Metadata = {
+ title: "Create T3 App",
+ description: "Generated by proofkit",
+ icons: [{ rel: "icon", url: "/favicon.ico" }],
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{ children: React.ReactNode }>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/layout/with-tw.tsx b/packages/cli-old/template/extras/src/app/layout/with-tw.tsx
new file mode 100644
index 00000000..5dea6caf
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/layout/with-tw.tsx
@@ -0,0 +1,20 @@
+import "~/styles/globals.css";
+
+import { GeistSans } from "geist/font/sans";
+import { type Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Create T3 App",
+ description: "Generated by proofkit",
+ icons: [{ rel: "icon", url: "/favicon.ico" }],
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{ children: React.ReactNode }>) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/next-auth/layout.tsx b/packages/cli-old/template/extras/src/app/next-auth/layout.tsx
new file mode 100644
index 00000000..51933f24
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/next-auth/layout.tsx
@@ -0,0 +1,22 @@
+import { auth } from "@/server/auth";
+import { Card, Center } from "@mantine/core";
+import { redirect } from "next/navigation";
+import React from "react";
+
+export default async function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const session = await auth();
+ if (session) {
+ return redirect("/");
+ }
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx b/packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx
new file mode 100644
index 00000000..b4781c4d
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx
@@ -0,0 +1,83 @@
+import { providerMap, signIn } from "@/server/auth";
+import {
+ Button,
+ Card,
+ Divider,
+ PasswordInput,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
+import { AuthError } from "next-auth";
+import Link from "next/link";
+import { redirect } from "next/navigation";
+
+export default async function SignInPage(props: {
+ searchParams: Promise<{ callbackUrl: string | undefined }>;
+}) {
+ const searchParams = await props.searchParams;
+ return (
+
+
+ {providerMap.length > 0 && (
+ <>
+
+ {Object.values(providerMap).map((provider) => (
+
+ ))}
+ >
+ )}
+
+
+ {"Don't have an account? "}
+ Sign up
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/next-auth/signup/action.ts b/packages/cli-old/template/extras/src/app/next-auth/signup/action.ts
new file mode 100644
index 00000000..fba6508d
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/next-auth/signup/action.ts
@@ -0,0 +1,24 @@
+"use server";
+
+import { signIn } from "@/server/auth";
+import { userSignUp } from "@/server/data/users";
+import { actionClient } from "@/server/safe-action";
+
+import { signUpSchema } from "./validation";
+
+export const signUpAction = actionClient
+ .schema(signUpSchema)
+ .action(async ({ parsedInput, ctx }) => {
+ const { email, password } = parsedInput;
+
+ await userSignUp({ email, password });
+
+ await signIn("credentials", {
+ email,
+ password,
+ });
+
+ return {
+ success: true,
+ };
+ });
diff --git a/packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx b/packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx
new file mode 100644
index 00000000..faab245f
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Button, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
+import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
+import Link from "next/link";
+import React from "react";
+
+import { signUpAction } from "./action";
+import { signUpSchema } from "./validation";
+
+export default function SignUpPage(props: {
+ searchParams: Promise<{ callbackUrl: string | undefined }>;
+}) {
+ const { form, action, handleSubmitWithAction, resetFormAndAction } =
+ useHookFormAction(signUpAction, zodResolver(signUpSchema), {
+ actionProps: {},
+ formProps: {},
+ errorMapProps: {},
+ });
+
+ return (
+
+
+
+ Already have an account? Sign in
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts b/packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts
new file mode 100644
index 00000000..d3086d30
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts
@@ -0,0 +1,12 @@
+import { z } from "zod/v4";
+
+export const signUpSchema = z
+ .object({
+ email: z.string().email(),
+ password: z.string(),
+ passwordConfirm: z.string(),
+ })
+ .refine((data) => data.password === data.passwordConfirm, {
+ message: "Passwords don't match",
+ path: ["passwordConfirm"],
+ });
diff --git a/packages/cli-old/template/extras/src/app/page/base.tsx b/packages/cli-old/template/extras/src/app/page/base.tsx
new file mode 100644
index 00000000..bf905890
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/page/base.tsx
@@ -0,0 +1,6 @@
+import { Text } from "@mantine/core";
+import Link from "next/link";
+
+export default function Home() {
+ return Welcome!;
+}
diff --git a/packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx b/packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx
new file mode 100644
index 00000000..49c9bbbe
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx
@@ -0,0 +1,67 @@
+import Link from "next/link";
+
+import { LatestPost } from "~/app/_components/post";
+import { getServerAuthSession } from "~/server/auth";
+import { api, HydrateClient } from "~/trpc/server";
+
+export default async function Home() {
+ const hello = await api.post.hello({ text: "from tRPC" });
+ const session = await getServerAuthSession();
+
+ void api.post.getLatest.prefetch();
+
+ return (
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+
+ {hello ? hello.greeting : "Loading tRPC query..."}
+
+
+
+
+ {session && Logged in as {session.user?.name}}
+
+
+ {session ? "Sign out" : "Sign in"}
+
+
+
+
+ {session?.user &&
}
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx b/packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx
new file mode 100644
index 00000000..cfeed2f5
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx
@@ -0,0 +1,68 @@
+import Link from "next/link";
+
+import { LatestPost } from "~/app/_components/post";
+import { getServerAuthSession } from "~/server/auth";
+import { api, HydrateClient } from "~/trpc/server";
+import styles from "./index.module.css";
+
+export default async function Home() {
+ const hello = await api.post.hello({ text: "from tRPC" });
+ const session = await getServerAuthSession();
+
+ void api.post.getLatest.prefetch();
+
+ return (
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+
+ {hello ? hello.greeting : "Loading tRPC query..."}
+
+
+
+
+ {session && Logged in as {session.user?.name}}
+
+
+ {session ? "Sign out" : "Sign in"}
+
+
+
+
+ {session?.user &&
}
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx b/packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx
new file mode 100644
index 00000000..d7121d83
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx
@@ -0,0 +1,53 @@
+import Link from "next/link";
+
+import { LatestPost } from "~/app/_components/post";
+import { api, HydrateClient } from "~/trpc/server";
+
+export default async function Home() {
+ const hello = await api.post.hello({ text: "from tRPC" });
+
+ void api.post.getLatest.prefetch();
+
+ return (
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+
+ {hello ? hello.greeting : "Loading tRPC query..."}
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/page/with-trpc.tsx b/packages/cli-old/template/extras/src/app/page/with-trpc.tsx
new file mode 100644
index 00000000..035f250b
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/page/with-trpc.tsx
@@ -0,0 +1,54 @@
+import Link from "next/link";
+
+import { LatestPost } from "~/app/_components/post";
+import { api, HydrateClient } from "~/trpc/server";
+import styles from "./index.module.css";
+
+export default async function Home() {
+ const hello = await api.post.hello({ text: "from tRPC" });
+
+ void api.post.getLatest.prefetch();
+
+ return (
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+
+ {hello ? hello.greeting : "Loading tRPC query..."}
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/app/page/with-tw.tsx b/packages/cli-old/template/extras/src/app/page/with-tw.tsx
new file mode 100644
index 00000000..773fef1b
--- /dev/null
+++ b/packages/cli-old/template/extras/src/app/page/with-tw.tsx
@@ -0,0 +1,37 @@
+import Link from "next/link";
+
+export default function HomePage() {
+ return (
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how to
+ deploy it.
+
+
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx b/packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx
new file mode 100644
index 00000000..50e2f512
--- /dev/null
+++ b/packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx
@@ -0,0 +1,18 @@
+"use client";
+
+import { ClerkProvider } from "@clerk/nextjs";
+import { dark } from "@clerk/themes";
+import { useComputedColorScheme } from "@mantine/core";
+
+export function ClerkAuthProvider({ children }: { children: React.ReactNode }) {
+ const computedColorScheme = useComputedColorScheme();
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx b/packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx
new file mode 100644
index 00000000..33683736
--- /dev/null
+++ b/packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { useClerk, useUser } from "@clerk/nextjs";
+import { Menu } from "@mantine/core";
+import { useRouter } from "next/navigation";
+import React from "react";
+
+/**
+ * Shown in the mobile header menu
+ */
+export default function UserMenuMobile() {
+ const { isSignedIn, isLoaded, user } = useUser();
+ const { signOut, buildSignInUrl } = useClerk();
+ const router = useRouter();
+
+ if (!isLoaded) return null;
+
+ if (!isSignedIn)
+ return (
+ <>
+
+ router.push(buildSignInUrl())}>
+ Sign In
+
+ >
+ );
+
+ if (isSignedIn)
+ return (
+ <>
+
+ {user.primaryEmailAddress?.emailAddress}
+ signOut()}>Sign Out
+ >
+ );
+}
diff --git a/packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx b/packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx
new file mode 100644
index 00000000..6f8da571
--- /dev/null
+++ b/packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { useClerk, UserButton, useUser } from "@clerk/nextjs";
+import { Button } from "@mantine/core";
+import { useRouter } from "next/navigation";
+
+export default function UserMenu() {
+ const { isSignedIn, isLoaded } = useUser();
+ const { buildSignInUrl } = useClerk();
+ const router = useRouter();
+
+ if (!isLoaded) return null;
+
+ if (!isSignedIn)
+ return (
+
+ );
+
+ if (isSignedIn) return ;
+
+ return null;
+}
diff --git a/packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx b/packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx
new file mode 100644
index 00000000..e6f328f4
--- /dev/null
+++ b/packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { Session } from "next-auth";
+import { SessionProvider } from "next-auth/react";
+
+export function NextAuthProvider({
+ children,
+ session,
+}: {
+ children: React.ReactNode;
+ session: Session | null | undefined;
+}) {
+ return {children};
+}
diff --git a/packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx b/packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx
new file mode 100644
index 00000000..5cadae53
--- /dev/null
+++ b/packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { Menu } from "@mantine/core";
+import { signIn, signOut, useSession } from "next-auth/react";
+import React from "react";
+
+/**
+ * Shown in the mobile header menu
+ */
+export default function UserMenuMobile() {
+ const { data: session, status } = useSession();
+
+ if (status === "loading") return null;
+
+ if (status === "unauthenticated")
+ return (
+ <>
+
+ signIn()}>Sign In
+ >
+ );
+
+ if (status === "authenticated")
+ return (
+ <>
+
+ {session.user.email}
+ signOut()}>Sign Out
+ >
+ );
+}
diff --git a/packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx b/packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx
new file mode 100644
index 00000000..a1305c5d
--- /dev/null
+++ b/packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { Button, Menu, px } from "@mantine/core";
+import { IconChevronDown } from "@tabler/icons-react";
+import { signIn, signOut, useSession } from "next-auth/react";
+
+export default function UserMenu() {
+ const { data: session, status } = useSession();
+
+ if (status === "loading") return null;
+
+ if (status === "unauthenticated")
+ return (
+
+ );
+
+ if (status === "authenticated")
+ return (
+
+ );
+
+ return null;
+}
diff --git a/packages/cli-old/template/extras/src/env/with-auth.ts b/packages/cli-old/template/extras/src/env/with-auth.ts
new file mode 100644
index 00000000..e73bf132
--- /dev/null
+++ b/packages/cli-old/template/extras/src/env/with-auth.ts
@@ -0,0 +1,31 @@
+import { createEnv } from "@t3-oss/env-nextjs";
+import { z } from "zod/v4";
+
+export const env = createEnv({
+ server: {
+ NODE_ENV: z
+ .enum(["development", "test", "production"])
+ .default("development"),
+ FM_DATABASE: z.string().endsWith(".fmp12"),
+ FM_SERVER: z.string().url(),
+ OTTO_API_KEY: z.string().startsWith("dk_"),
+
+ // Next Auth
+ NEXTAUTH_SECRET:
+ process.env.NODE_ENV === "production"
+ ? z.string()
+ : z.string().optional(),
+ NEXTAUTH_URL: z.preprocess(
+ // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
+ // Since NextAuth.js automatically uses the VERCEL_URL if present.
+ (str) => process.env.VERCEL_URL ?? str,
+ // VERCEL_URL doesn't include `https` so it cant be validated as a URL
+ process.env.VERCEL ? z.string() : z.string().url()
+ ),
+ DISCORD_CLIENT_ID: z.string(),
+ DISCORD_CLIENT_SECRET: z.string(),
+ },
+ client: {},
+ // For Next.js >= 13.4.4, you only need to destructure client variables:
+ experimental__runtimeEnv: {},
+});
diff --git a/packages/cli-old/template/extras/src/env/with-clerk.ts b/packages/cli-old/template/extras/src/env/with-clerk.ts
new file mode 100644
index 00000000..e9825af7
--- /dev/null
+++ b/packages/cli-old/template/extras/src/env/with-clerk.ts
@@ -0,0 +1,20 @@
+import { createEnv } from "@t3-oss/env-nextjs";
+import { z } from "zod/v4";
+
+export const env = createEnv({
+ server: {
+ NODE_ENV: z
+ .enum(["development", "test", "production"])
+ .default("development"),
+ FM_DATABASE: z.string().endsWith(".fmp12"),
+ FM_SERVER: z.string().url(),
+ OTTO_API_KEY: z.string().startsWith("dk_"),
+
+ // Clerk
+ CLERK_SECRET_KEY: z.string().min(1),
+ CLERK_WEBHOOK_SECRET: z.string().min(1),
+ },
+ client: {},
+ // For Next.js >= 13.4.4, you only need to destructure client variables:
+ experimental__runtimeEnv: {},
+});
diff --git a/packages/cli-old/template/extras/src/index.module.css b/packages/cli-old/template/extras/src/index.module.css
new file mode 100644
index 00000000..fac9982a
--- /dev/null
+++ b/packages/cli-old/template/extras/src/index.module.css
@@ -0,0 +1,177 @@
+.main {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ background-image: linear-gradient(to bottom, #2e026d, #15162c);
+}
+
+.container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 3rem;
+ padding: 4rem 1rem;
+}
+
+@media (min-width: 640px) {
+ .container {
+ max-width: 640px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 768px;
+ }
+}
+
+@media (min-width: 1024px) {
+ .container {
+ max-width: 1024px;
+ }
+}
+
+@media (min-width: 1280px) {
+ .container {
+ max-width: 1280px;
+ }
+}
+
+@media (min-width: 1536px) {
+ .container {
+ max-width: 1536px;
+ }
+}
+
+.title {
+ font-size: 3rem;
+ line-height: 1;
+ font-weight: 800;
+ letter-spacing: -0.025em;
+ margin: 0;
+ color: white;
+}
+
+@media (min-width: 640px) {
+ .title {
+ font-size: 5rem;
+ }
+}
+
+.pinkSpan {
+ color: hsl(280 100% 70%);
+}
+
+.cardRow {
+ display: grid;
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+@media (min-width: 640px) {
+ .cardRow {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (min-width: 768px) {
+ .cardRow {
+ gap: 2rem;
+ }
+}
+
+.card {
+ max-width: 20rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ padding: 1rem;
+ border-radius: 0.75rem;
+ color: white;
+ background-color: rgb(255 255 255 / 0.1);
+}
+
+.card:hover {
+ background-color: rgb(255 255 255 / 0.2);
+ transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1);
+}
+
+.cardTitle {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ font-weight: 700;
+ margin: 0;
+}
+
+.cardText {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+}
+
+.showcaseContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.showcaseText {
+ color: white;
+ text-align: center;
+ font-size: 1.5rem;
+ line-height: 2rem;
+}
+
+.authContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+}
+
+.loginButton {
+ border-radius: 9999px;
+ background-color: rgb(255 255 255 / 0.1);
+ padding: 0.75rem 2.5rem;
+ font-weight: 600;
+ color: white;
+ text-decoration-line: none;
+ transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1);
+}
+
+.loginButton:hover {
+ background-color: rgb(255 255 255 / 0.2);
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.input {
+ width: 100%;
+ border-radius: 9999px;
+ padding: 0.5rem 1rem;
+ color: black;
+}
+
+.submitButton {
+ all: unset;
+ border-radius: 9999px;
+ background-color: rgb(255 255 255 / 0.1);
+ padding: 0.75rem 2.5rem;
+ font-weight: 600;
+ color: white;
+ text-align: center;
+ transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1);
+}
+
+.submitButton:hover {
+ background-color: rgb(255 255 255 / 0.2);
+}
diff --git a/packages/cli-old/template/extras/src/middleware/clerk.ts b/packages/cli-old/template/extras/src/middleware/clerk.ts
new file mode 100644
index 00000000..1dd75bb4
--- /dev/null
+++ b/packages/cli-old/template/extras/src/middleware/clerk.ts
@@ -0,0 +1,20 @@
+import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
+
+// these default settings will require authentication for all routes except the ones in the array
+// to restrict public access to the home page, remove "/" from the array
+const isPublicRoute = createRouteMatcher(["/auth/(.*)", "/"]);
+
+export default clerkMiddleware(async (auth, request) => {
+ if (!isPublicRoute(request)) {
+ await auth.protect();
+ }
+});
+
+export const config = {
+ matcher: [
+ // Skip Next.js internals and all static files, unless found in search params
+ "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
+ // Always run for API routes
+ "/(api|trpc)(.*)",
+ ],
+};
diff --git a/packages/cli-old/template/extras/src/middleware/next-auth.ts b/packages/cli-old/template/extras/src/middleware/next-auth.ts
new file mode 100644
index 00000000..e1f450d4
--- /dev/null
+++ b/packages/cli-old/template/extras/src/middleware/next-auth.ts
@@ -0,0 +1,5 @@
+export { auth as middleware } from "@/server/auth";
+
+export const config = {
+ matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
+};
diff --git a/packages/cli-old/template/extras/src/pages/_app/base.tsx b/packages/cli-old/template/extras/src/pages/_app/base.tsx
new file mode 100644
index 00000000..e7e7fb29
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/_app/base.tsx
@@ -0,0 +1,14 @@
+import { GeistSans } from "geist/font/sans";
+import { type AppType } from "next/dist/shared/lib/utils";
+
+import "~/styles/globals.css";
+
+const MyApp: AppType = ({ Component, pageProps }) => {
+ return (
+
+
+
+ );
+};
+
+export default MyApp;
diff --git a/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx b/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx
new file mode 100644
index 00000000..89d10b0c
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx
@@ -0,0 +1,23 @@
+import { GeistSans } from "geist/font/sans";
+import { type Session } from "next-auth";
+import { SessionProvider } from "next-auth/react";
+import { type AppType } from "next/app";
+
+import { api } from "~/utils/api";
+
+import "~/styles/globals.css";
+
+const MyApp: AppType<{ session: Session | null }> = ({
+ Component,
+ pageProps: { session, ...pageProps },
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default api.withTRPC(MyApp);
diff --git a/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx b/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx
new file mode 100644
index 00000000..89d10b0c
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx
@@ -0,0 +1,23 @@
+import { GeistSans } from "geist/font/sans";
+import { type Session } from "next-auth";
+import { SessionProvider } from "next-auth/react";
+import { type AppType } from "next/app";
+
+import { api } from "~/utils/api";
+
+import "~/styles/globals.css";
+
+const MyApp: AppType<{ session: Session | null }> = ({
+ Component,
+ pageProps: { session, ...pageProps },
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default api.withTRPC(MyApp);
diff --git a/packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx b/packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx
new file mode 100644
index 00000000..a008ed16
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx
@@ -0,0 +1,21 @@
+import { GeistSans } from "geist/font/sans";
+import { type Session } from "next-auth";
+import { SessionProvider } from "next-auth/react";
+import { type AppType } from "next/app";
+
+import "~/styles/globals.css";
+
+const MyApp: AppType<{ session: Session | null }> = ({
+ Component,
+ pageProps: { session, ...pageProps },
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default MyApp;
diff --git a/packages/cli-old/template/extras/src/pages/_app/with-auth.tsx b/packages/cli-old/template/extras/src/pages/_app/with-auth.tsx
new file mode 100644
index 00000000..a008ed16
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/_app/with-auth.tsx
@@ -0,0 +1,21 @@
+import { GeistSans } from "geist/font/sans";
+import { type Session } from "next-auth";
+import { SessionProvider } from "next-auth/react";
+import { type AppType } from "next/app";
+
+import "~/styles/globals.css";
+
+const MyApp: AppType<{ session: Session | null }> = ({
+ Component,
+ pageProps: { session, ...pageProps },
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default MyApp;
diff --git a/packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx b/packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx
new file mode 100644
index 00000000..464c50cc
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx
@@ -0,0 +1,16 @@
+import { GeistSans } from "geist/font/sans";
+import { type AppType } from "next/app";
+
+import { api } from "~/utils/api";
+
+import "~/styles/globals.css";
+
+const MyApp: AppType = ({ Component, pageProps }) => {
+ return (
+
+
+
+ );
+};
+
+export default api.withTRPC(MyApp);
diff --git a/packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx b/packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx
new file mode 100644
index 00000000..464c50cc
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx
@@ -0,0 +1,16 @@
+import { GeistSans } from "geist/font/sans";
+import { type AppType } from "next/app";
+
+import { api } from "~/utils/api";
+
+import "~/styles/globals.css";
+
+const MyApp: AppType = ({ Component, pageProps }) => {
+ return (
+
+
+
+ );
+};
+
+export default api.withTRPC(MyApp);
diff --git a/packages/cli-old/template/extras/src/pages/_app/with-tw.tsx b/packages/cli-old/template/extras/src/pages/_app/with-tw.tsx
new file mode 100644
index 00000000..da39269a
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/_app/with-tw.tsx
@@ -0,0 +1,14 @@
+import { GeistSans } from "geist/font/sans";
+import { type AppType } from "next/app";
+
+import "~/styles/globals.css";
+
+const MyApp: AppType = ({ Component, pageProps }) => {
+ return (
+
+
+
+ );
+};
+
+export default MyApp;
diff --git a/packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts b/packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts
new file mode 100644
index 00000000..8739530f
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts
@@ -0,0 +1,5 @@
+import NextAuth from "next-auth";
+
+import { authOptions } from "~/server/auth";
+
+export default NextAuth(authOptions);
diff --git a/packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts b/packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts
new file mode 100644
index 00000000..587dd2bd
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts
@@ -0,0 +1,19 @@
+import { createNextApiHandler } from "@trpc/server/adapters/next";
+
+import { env } from "~/env";
+import { appRouter } from "~/server/api/root";
+import { createTRPCContext } from "~/server/api/trpc";
+
+// export API handler
+export default createNextApiHandler({
+ router: appRouter,
+ createContext: createTRPCContext,
+ onError:
+ env.NODE_ENV === "development"
+ ? ({ path, error }) => {
+ console.error(
+ `❌ tRPC failed on ${path ?? ""}: ${error.message}`
+ );
+ }
+ : undefined,
+});
diff --git a/packages/cli-old/template/extras/src/pages/index/base.tsx b/packages/cli-old/template/extras/src/pages/index/base.tsx
new file mode 100644
index 00000000..a34888c6
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/index/base.tsx
@@ -0,0 +1,47 @@
+import Head from "next/head";
+import Link from "next/link";
+
+import styles from "./index.module.css";
+
+export default function Home() {
+ return (
+ <>
+
+ Create T3 App
+
+
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx b/packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx
new file mode 100644
index 00000000..532e7f73
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx
@@ -0,0 +1,80 @@
+import { signIn, signOut, useSession } from "next-auth/react";
+import Head from "next/head";
+import Link from "next/link";
+
+import { api } from "~/utils/api";
+
+export default function Home() {
+ const hello = api.post.hello.useQuery({ text: "from tRPC" });
+
+ return (
+ <>
+
+ Create T3 App
+
+
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+
+ {hello.data ? hello.data.greeting : "Loading tRPC query..."}
+
+
+
+
+
+ >
+ );
+}
+
+function AuthShowcase() {
+ const { data: sessionData } = useSession();
+
+ const { data: secretMessage } = api.post.getSecretMessage.useQuery(
+ undefined, // no input
+ { enabled: sessionData?.user !== undefined }
+ );
+
+ return (
+
+
+ {sessionData && Logged in as {sessionData.user?.name}}
+ {secretMessage && - {secretMessage}}
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx b/packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx
new file mode 100644
index 00000000..f3191246
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx
@@ -0,0 +1,81 @@
+import { signIn, signOut, useSession } from "next-auth/react";
+import Head from "next/head";
+import Link from "next/link";
+
+import { api } from "~/utils/api";
+import styles from "./index.module.css";
+
+export default function Home() {
+ const hello = api.post.hello.useQuery({ text: "from tRPC" });
+
+ return (
+ <>
+
+ Create T3 App
+
+
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+
+ {hello.data ? hello.data.greeting : "Loading tRPC query..."}
+
+
+
+
+
+ >
+ );
+}
+
+function AuthShowcase() {
+ const { data: sessionData } = useSession();
+
+ const { data: secretMessage } = api.post.getSecretMessage.useQuery(
+ undefined, // no input
+ { enabled: sessionData?.user !== undefined }
+ );
+
+ return (
+
+
+ {sessionData && Logged in as {sessionData.user?.name}}
+ {secretMessage && - {secretMessage}}
+
+
+
+ );
+}
diff --git a/packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx b/packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx
new file mode 100644
index 00000000..3a51c3e8
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx
@@ -0,0 +1,52 @@
+import Head from "next/head";
+import Link from "next/link";
+
+import { api } from "~/utils/api";
+
+export default function Home() {
+ const hello = api.post.hello.useQuery({ text: "from tRPC" });
+
+ return (
+ <>
+
+ Create T3 App
+
+
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+ {hello.data ? hello.data.greeting : "Loading tRPC query..."}
+
+
+
+ >
+ );
+}
diff --git a/packages/cli-old/template/extras/src/pages/index/with-trpc.tsx b/packages/cli-old/template/extras/src/pages/index/with-trpc.tsx
new file mode 100644
index 00000000..26d807f9
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/index/with-trpc.tsx
@@ -0,0 +1,53 @@
+import Head from "next/head";
+import Link from "next/link";
+
+import { api } from "~/utils/api";
+import styles from "./index.module.css";
+
+export default function Home() {
+ const hello = api.post.hello.useQuery({ text: "from tRPC" });
+
+ return (
+ <>
+
+ Create T3 App
+
+
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+ {hello.data ? hello.data.greeting : "Loading tRPC query..."}
+
+
+
+ >
+ );
+}
diff --git a/packages/cli-old/template/extras/src/pages/index/with-tw.tsx b/packages/cli-old/template/extras/src/pages/index/with-tw.tsx
new file mode 100644
index 00000000..88b818e2
--- /dev/null
+++ b/packages/cli-old/template/extras/src/pages/index/with-tw.tsx
@@ -0,0 +1,45 @@
+import Head from "next/head";
+import Link from "next/link";
+
+export default function Home() {
+ return (
+ <>
+
+ Create T3 App
+
+
+
+
+
+
+ Create T3 App
+
+
+
+
First Steps →
+
+ Just the basics - Everything you need to know to set up your
+ database and authentication.
+
+
+
+
Documentation →
+
+ Learn more about Create T3 App, the libraries it uses, and how
+ to deploy it.
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/cli-old/template/extras/src/server/api/root.ts b/packages/cli-old/template/extras/src/server/api/root.ts
new file mode 100644
index 00000000..b341fc4d
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/root.ts
@@ -0,0 +1,23 @@
+import { postRouter } from "~/server/api/routers/post";
+import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
+
+/**
+ * This is the primary router for your server.
+ *
+ * All routers added in /api/routers should be manually added here.
+ */
+export const appRouter = createTRPCRouter({
+ post: postRouter,
+});
+
+// export type definition of API
+export type AppRouter = typeof appRouter;
+
+/**
+ * Create a server-side caller for the tRPC API.
+ * @example
+ * const trpc = createCaller(createContext);
+ * const res = await trpc.post.all();
+ * ^? Post[]
+ */
+export const createCaller = createCallerFactory(appRouter);
diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/base.ts b/packages/cli-old/template/extras/src/server/api/routers/post/base.ts
new file mode 100644
index 00000000..6781c531
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/routers/post/base.ts
@@ -0,0 +1,40 @@
+import { z } from "zod/v4";
+
+import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
+
+// Mocked DB
+interface Post {
+ id: number;
+ name: string;
+}
+const posts: Post[] = [
+ {
+ id: 1,
+ name: "Hello World",
+ },
+];
+
+export const postRouter = createTRPCRouter({
+ hello: publicProcedure
+ .input(z.object({ text: z.string() }))
+ .query(({ input }) => {
+ return {
+ greeting: `Hello ${input.text}`,
+ };
+ }),
+
+ create: publicProcedure
+ .input(z.object({ name: z.string().min(1) }))
+ .mutation(async ({ input }) => {
+ const post: Post = {
+ id: posts.length + 1,
+ name: input.name,
+ };
+ posts.push(post);
+ return post;
+ }),
+
+ getLatest: publicProcedure.query(() => {
+ return posts.at(-1) ?? null;
+ }),
+});
diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts
new file mode 100644
index 00000000..35ac7ba8
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts
@@ -0,0 +1,39 @@
+import { z } from "zod/v4";
+
+import {
+ createTRPCRouter,
+ protectedProcedure,
+ publicProcedure,
+} from "~/server/api/trpc";
+import { posts } from "~/server/db/schema";
+
+export const postRouter = createTRPCRouter({
+ hello: publicProcedure
+ .input(z.object({ text: z.string() }))
+ .query(({ input }) => {
+ return {
+ greeting: `Hello ${input.text}`,
+ };
+ }),
+
+ create: protectedProcedure
+ .input(z.object({ name: z.string().min(1) }))
+ .mutation(async ({ ctx, input }) => {
+ await ctx.db.insert(posts).values({
+ name: input.name,
+ createdById: ctx.session.user.id,
+ });
+ }),
+
+ getLatest: publicProcedure.query(async ({ ctx }) => {
+ const post = await ctx.db.query.posts.findFirst({
+ orderBy: (posts, { desc }) => [desc(posts.createdAt)],
+ });
+
+ return post ?? null;
+ }),
+
+ getSecretMessage: protectedProcedure.query(() => {
+ return "you can now see this secret message!";
+ }),
+});
diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts
new file mode 100644
index 00000000..f0140b76
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts
@@ -0,0 +1,41 @@
+import { z } from "zod/v4";
+
+import {
+ createTRPCRouter,
+ protectedProcedure,
+ publicProcedure,
+} from "~/server/api/trpc";
+
+export const postRouter = createTRPCRouter({
+ hello: publicProcedure
+ .input(z.object({ text: z.string() }))
+ .query(({ input }) => {
+ return {
+ greeting: `Hello ${input.text}`,
+ };
+ }),
+
+ create: protectedProcedure
+ .input(z.object({ name: z.string().min(1) }))
+ .mutation(async ({ ctx, input }) => {
+ return ctx.db.post.create({
+ data: {
+ name: input.name,
+ createdBy: { connect: { id: ctx.session.user.id } },
+ },
+ });
+ }),
+
+ getLatest: protectedProcedure.query(async ({ ctx }) => {
+ const post = await ctx.db.post.findFirst({
+ orderBy: { createdAt: "desc" },
+ where: { createdBy: { id: ctx.session.user.id } },
+ });
+
+ return post ?? null;
+ }),
+
+ getSecretMessage: protectedProcedure.query(() => {
+ return "you can now see this secret message!";
+ }),
+});
diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts
new file mode 100644
index 00000000..8ea389bf
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts
@@ -0,0 +1,37 @@
+import { z } from "zod/v4";
+
+import {
+ createTRPCRouter,
+ protectedProcedure,
+ publicProcedure,
+} from "~/server/api/trpc";
+
+let post = {
+ id: 1,
+ name: "Hello World",
+};
+
+export const postRouter = createTRPCRouter({
+ hello: publicProcedure
+ .input(z.object({ text: z.string() }))
+ .query(({ input }) => {
+ return {
+ greeting: `Hello ${input.text}`,
+ };
+ }),
+
+ create: protectedProcedure
+ .input(z.object({ name: z.string().min(1) }))
+ .mutation(async ({ input }) => {
+ post = { id: post.id + 1, name: input.name };
+ return post;
+ }),
+
+ getLatest: protectedProcedure.query(() => {
+ return post;
+ }),
+
+ getSecretMessage: protectedProcedure.query(() => {
+ return "you can now see this secret message!";
+ }),
+});
diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts
new file mode 100644
index 00000000..a295842c
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts
@@ -0,0 +1,30 @@
+import { z } from "zod/v4";
+
+import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
+import { posts } from "~/server/db/schema";
+
+export const postRouter = createTRPCRouter({
+ hello: publicProcedure
+ .input(z.object({ text: z.string() }))
+ .query(({ input }) => {
+ return {
+ greeting: `Hello ${input.text}`,
+ };
+ }),
+
+ create: publicProcedure
+ .input(z.object({ name: z.string().min(1) }))
+ .mutation(async ({ ctx, input }) => {
+ await ctx.db.insert(posts).values({
+ name: input.name,
+ });
+ }),
+
+ getLatest: publicProcedure.query(async ({ ctx }) => {
+ const post = await ctx.db.query.posts.findFirst({
+ orderBy: (posts, { desc }) => [desc(posts.createdAt)],
+ });
+
+ return post ?? null;
+ }),
+});
diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts
new file mode 100644
index 00000000..3282fa6e
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts
@@ -0,0 +1,31 @@
+import { z } from "zod/v4";
+
+import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
+
+export const postRouter = createTRPCRouter({
+ hello: publicProcedure
+ .input(z.object({ text: z.string() }))
+ .query(({ input }) => {
+ return {
+ greeting: `Hello ${input.text}`,
+ };
+ }),
+
+ create: publicProcedure
+ .input(z.object({ name: z.string().min(1) }))
+ .mutation(async ({ ctx, input }) => {
+ return ctx.db.post.create({
+ data: {
+ name: input.name,
+ },
+ });
+ }),
+
+ getLatest: publicProcedure.query(async ({ ctx }) => {
+ const post = await ctx.db.post.findFirst({
+ orderBy: { createdAt: "desc" },
+ });
+
+ return post ?? null;
+ }),
+});
diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts
new file mode 100644
index 00000000..e831d1a8
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts
@@ -0,0 +1,103 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+import { initTRPC } from "@trpc/server";
+import superjson from "superjson";
+import { ZodError } from "zod/v4";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ *
+ * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
+ * wrap this and provides the required context.
+ *
+ * @see https://trpc.io/docs/server/context
+ */
+export const createTRPCContext = async (opts: { headers: Headers }) => {
+ return {
+ ...opts,
+ };
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an articifial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts
new file mode 100644
index 00000000..a8ca5724
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts
@@ -0,0 +1,133 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+
+import { initTRPC, TRPCError } from "@trpc/server";
+import superjson from "superjson";
+import { ZodError } from "zod/v4";
+
+import { getServerAuthSession } from "~/server/auth";
+import { db } from "~/server/db";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ *
+ * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
+ * wrap this and provides the required context.
+ *
+ * @see https://trpc.io/docs/server/context
+ */
+export const createTRPCContext = async (opts: { headers: Headers }) => {
+ const session = await getServerAuthSession();
+
+ return {
+ db,
+ session,
+ ...opts,
+ };
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an articifial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
+
+/**
+ * Protected (authenticated) procedure
+ *
+ * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
+ * the session is valid and guarantees `ctx.session.user` is not null.
+ *
+ * @see https://trpc.io/docs/procedures
+ */
+export const protectedProcedure = t.procedure
+ .use(timingMiddleware)
+ .use(({ ctx, next }) => {
+ if (!ctx.session || !ctx.session.user) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
+ return next({
+ ctx: {
+ // infers the `session` as non-nullable
+ session: { ...ctx.session, user: ctx.session.user },
+ },
+ });
+ });
diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts
new file mode 100644
index 00000000..55e20cf1
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts
@@ -0,0 +1,130 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+import { initTRPC, TRPCError } from "@trpc/server";
+import superjson from "superjson";
+import { ZodError } from "zod/v4";
+
+import { getServerAuthSession } from "~/server/auth";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ *
+ * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
+ * wrap this and provides the required context.
+ *
+ * @see https://trpc.io/docs/server/context
+ */
+export const createTRPCContext = async (opts: { headers: Headers }) => {
+ const session = await getServerAuthSession();
+
+ return {
+ session,
+ ...opts,
+ };
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an articifial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
+
+/**
+ * Protected (authenticated) procedure
+ *
+ * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
+ * the session is valid and guarantees `ctx.session.user` is not null.
+ *
+ * @see https://trpc.io/docs/procedures
+ */
+export const protectedProcedure = t.procedure
+ .use(timingMiddleware)
+ .use(({ ctx, next }) => {
+ if (!ctx.session || !ctx.session.user) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
+ return next({
+ ctx: {
+ // infers the `session` as non-nullable
+ session: { ...ctx.session, user: ctx.session.user },
+ },
+ });
+ });
diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts
new file mode 100644
index 00000000..c77a7530
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts
@@ -0,0 +1,106 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+import { initTRPC } from "@trpc/server";
+import superjson from "superjson";
+import { ZodError } from "zod/v4";
+
+import { db } from "~/server/db";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ *
+ * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
+ * wrap this and provides the required context.
+ *
+ * @see https://trpc.io/docs/server/context
+ */
+export const createTRPCContext = async (opts: { headers: Headers }) => {
+ return {
+ db,
+ ...opts,
+ };
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an articifial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts
new file mode 100644
index 00000000..b1d9f34c
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts
@@ -0,0 +1,122 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+
+import { initTRPC } from "@trpc/server";
+import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
+import superjson from "superjson";
+import { ZodError } from "zod/v4";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ */
+
+type CreateContextOptions = Record;
+
+/**
+ * This helper generates the "internals" for a tRPC context. If you need to use it, you can export
+ * it from here.
+ *
+ * Examples of things you may need it for:
+ * - testing, so we don't have to mock Next.js' req/res
+ * - tRPC's `createSSGHelpers`, where we don't have req/res
+ *
+ * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
+ */
+const createInnerTRPCContext = (_opts: CreateContextOptions) => {
+ return {};
+};
+
+/**
+ * This is the actual context you will use in your router. It will be used to process every request
+ * that goes through your tRPC endpoint.
+ *
+ * @see https://trpc.io/docs/context
+ */
+export const createTRPCContext = (_opts: CreateNextContextOptions) => {
+ return createInnerTRPCContext({});
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an articifial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts
new file mode 100644
index 00000000..f057f427
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts
@@ -0,0 +1,160 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+
+import { initTRPC, TRPCError } from "@trpc/server";
+import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
+import { type Session } from "next-auth";
+import superjson from "superjson";
+import { ZodError } from "zod/v4";
+
+import { getServerAuthSession } from "~/server/auth";
+import { db } from "~/server/db";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ */
+
+interface CreateContextOptions {
+ session: Session | null;
+}
+
+/**
+ * This helper generates the "internals" for a tRPC context. If you need to use it, you can export
+ * it from here.
+ *
+ * Examples of things you may need it for:
+ * - testing, so we don't have to mock Next.js' req/res
+ * - tRPC's `createSSGHelpers`, where we don't have req/res
+ *
+ * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
+ */
+const createInnerTRPCContext = (opts: CreateContextOptions) => {
+ return {
+ session: opts.session,
+ db,
+ };
+};
+
+/**
+ * This is the actual context you will use in your router. It will be used to process every request
+ * that goes through your tRPC endpoint.
+ *
+ * @see https://trpc.io/docs/context
+ */
+export const createTRPCContext = async (opts: CreateNextContextOptions) => {
+ const { req, res } = opts;
+
+ // Get the session from the server using the getServerSession wrapper function
+ const session = await getServerAuthSession({ req, res });
+
+ return createInnerTRPCContext({
+ session,
+ });
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an articifial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
+
+/**
+ * Protected (authenticated) procedure
+ *
+ * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
+ * the session is valid and guarantees `ctx.session.user` is not null.
+ *
+ * @see https://trpc.io/docs/procedures
+ */
+export const protectedProcedure = t.procedure
+ .use(timingMiddleware)
+ .use(({ ctx, next }) => {
+ if (!ctx.session || !ctx.session.user) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
+ return next({
+ ctx: {
+ // infers the `session` as non-nullable
+ session: { ...ctx.session, user: ctx.session.user },
+ },
+ });
+ });
diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts
new file mode 100644
index 00000000..87a6b0e2
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts
@@ -0,0 +1,158 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+import { initTRPC, TRPCError } from "@trpc/server";
+import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
+import { type Session } from "next-auth";
+import superjson from "superjson";
+import { ZodError } from "zod/v4";
+
+import { getServerAuthSession } from "~/server/auth";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ */
+
+interface CreateContextOptions {
+ session: Session | null;
+}
+
+/**
+ * This helper generates the "internals" for a tRPC context. If you need to use it, you can export
+ * it from here.
+ *
+ * Examples of things you may need it for:
+ * - testing, so we don't have to mock Next.js' req/res
+ * - tRPC's `createSSGHelpers`, where we don't have req/res
+ *
+ * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
+ */
+const createInnerTRPCContext = ({ session }: CreateContextOptions) => {
+ return {
+ session,
+ };
+};
+
+/**
+ * This is the actual context you will use in your router. It will be used to process every request
+ * that goes through your tRPC endpoint.
+ *
+ * @see https://trpc.io/docs/context
+ */
+export const createTRPCContext = async ({
+ req,
+ res,
+}: CreateNextContextOptions) => {
+ // Get the session from the server using the getServerSession wrapper function
+ const session = await getServerAuthSession({ req, res });
+
+ return createInnerTRPCContext({
+ session,
+ });
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an articifial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
+
+/**
+ * Protected (authenticated) procedure
+ *
+ * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
+ * the session is valid and guarantees `ctx.session.user` is not null.
+ *
+ * @see https://trpc.io/docs/procedures
+ */
+export const protectedProcedure = t.procedure
+ .use(timingMiddleware)
+ .use(({ ctx, next }) => {
+ if (!ctx.session || !ctx.session.user) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
+ return next({
+ ctx: {
+ // infers the `session` as non-nullable
+ session: { ...ctx.session, user: ctx.session.user },
+ },
+ });
+ });
diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts
new file mode 100644
index 00000000..a6e5bef7
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts
@@ -0,0 +1,125 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+import { initTRPC } from "@trpc/server";
+import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
+import superjson from "superjson";
+import { ZodError } from "zod/v4";
+
+import { db } from "~/server/db";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ */
+
+type CreateContextOptions = Record;
+
+/**
+ * This helper generates the "internals" for a tRPC context. If you need to use it, you can export
+ * it from here.
+ *
+ * Examples of things you may need it for:
+ * - testing, so we don't have to mock Next.js' req/res
+ * - tRPC's `createSSGHelpers`, where we don't have req/res
+ *
+ * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
+ */
+const createInnerTRPCContext = (_opts: CreateContextOptions) => {
+ return {
+ db,
+ };
+};
+
+/**
+ * This is the actual context you will use in your router. It will be used to process every request
+ * that goes through your tRPC endpoint.
+ *
+ * @see https://trpc.io/docs/context
+ */
+export const createTRPCContext = (_opts: CreateNextContextOptions) => {
+ return createInnerTRPCContext({});
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an articifial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
diff --git a/packages/cli-old/template/extras/src/server/data/users.ts b/packages/cli-old/template/extras/src/server/data/users.ts
new file mode 100644
index 00000000..fac1dc35
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/data/users.ts
@@ -0,0 +1,23 @@
+import "server-only";
+
+import { fmAdapter } from "../auth";
+import { saltAndHashPassword } from "../password";
+
+type UserSignUpInput = {
+ email: string;
+ password: string;
+};
+
+export async function userSignUp(input: UserSignUpInput) {
+ const passwordHash = await saltAndHashPassword(input.password);
+
+ // create the user in our database
+ const user = await fmAdapter.typedClients.userWithPasswordHash.create({
+ fieldData: {
+ email: input.email,
+ passwordHash,
+ },
+ });
+
+ return user;
+}
diff --git a/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts b/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts
new file mode 100644
index 00000000..52188938
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts
@@ -0,0 +1,22 @@
+import { Client } from "@planetscale/database";
+import { PrismaPlanetScale } from "@prisma/adapter-planetscale";
+import { PrismaClient } from "@prisma/client";
+
+import { env } from "~/env";
+
+const psClient = new Client({ url: env.DATABASE_URL });
+
+const createPrismaClient = () =>
+ new PrismaClient({
+ log:
+ env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
+ adapter: new PrismaPlanetScale(psClient),
+ });
+
+const globalForPrisma = globalThis as unknown as {
+ prisma: ReturnType | undefined;
+};
+
+export const db = globalForPrisma.prisma ?? createPrismaClient();
+
+if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
diff --git a/packages/cli-old/template/extras/src/server/db/db-prisma.ts b/packages/cli-old/template/extras/src/server/db/db-prisma.ts
new file mode 100644
index 00000000..07dc0271
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/db-prisma.ts
@@ -0,0 +1,17 @@
+import { PrismaClient } from "@prisma/client";
+
+import { env } from "~/env";
+
+const createPrismaClient = () =>
+ new PrismaClient({
+ log:
+ env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
+ });
+
+const globalForPrisma = globalThis as unknown as {
+ prisma: ReturnType | undefined;
+};
+
+export const db = globalForPrisma.prisma ?? createPrismaClient();
+
+if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts
new file mode 100644
index 00000000..3542b7b8
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts
@@ -0,0 +1,18 @@
+import { drizzle } from "drizzle-orm/mysql2";
+import { createPool, type Pool } from "mysql2/promise";
+
+import { env } from "~/env";
+import * as schema from "./schema";
+
+/**
+ * Cache the database connection in development. This avoids creating a new connection on every HMR
+ * update.
+ */
+const globalForDb = globalThis as unknown as {
+ conn: Pool | undefined;
+};
+
+const conn = globalForDb.conn ?? createPool({ uri: env.DATABASE_URL });
+if (env.NODE_ENV !== "production") globalForDb.conn = conn;
+
+export const db = drizzle(conn, { schema, mode: "default" });
diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts
new file mode 100644
index 00000000..4613a4c1
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts
@@ -0,0 +1,7 @@
+import { Client } from "@planetscale/database";
+import { drizzle } from "drizzle-orm/planetscale-serverless";
+
+import { env } from "~/env";
+import * as schema from "./schema";
+
+export const db = drizzle(new Client({ url: env.DATABASE_URL }), { schema });
diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts
new file mode 100644
index 00000000..1287189a
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts
@@ -0,0 +1,18 @@
+import { drizzle } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+
+import { env } from "~/env";
+import * as schema from "./schema";
+
+/**
+ * Cache the database connection in development. This avoids creating a new connection on every HMR
+ * update.
+ */
+const globalForDb = globalThis as unknown as {
+ conn: postgres.Sql | undefined;
+};
+
+const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
+if (env.NODE_ENV !== "production") globalForDb.conn = conn;
+
+export const db = drizzle(conn, { schema });
diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts
new file mode 100644
index 00000000..ef1df14a
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts
@@ -0,0 +1,19 @@
+import { createClient, type Client } from "@libsql/client";
+import { drizzle } from "drizzle-orm/libsql";
+
+import { env } from "~/env";
+import * as schema from "./schema";
+
+/**
+ * Cache the database connection in development. This avoids creating a new connection on every HMR
+ * update.
+ */
+const globalForDb = globalThis as unknown as {
+ client: Client | undefined;
+};
+
+export const client =
+ globalForDb.client ?? createClient({ url: env.DATABASE_URL });
+if (env.NODE_ENV !== "production") globalForDb.client = client;
+
+export const db = drizzle(client, { schema });
diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts
new file mode 100644
index 00000000..bfb08079
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts
@@ -0,0 +1,34 @@
+// Example model schema from the Drizzle docs
+// https://orm.drizzle.team/docs/sql-schema-declaration
+
+import { sql } from "drizzle-orm";
+import {
+ bigint,
+ index,
+ mysqlTableCreator,
+ timestamp,
+ varchar,
+} from "drizzle-orm/mysql-core";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = mysqlTableCreator((name) => `project1_${name}`);
+
+export const posts = createTable(
+ "post",
+ {
+ id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
+ name: varchar("name", { length: 256 }),
+ createdAt: timestamp("created_at")
+ .default(sql`CURRENT_TIMESTAMP`)
+ .notNull(),
+ updatedAt: timestamp("updated_at").onUpdateNow(),
+ },
+ (example) => ({
+ nameIndex: index("name_idx").on(example.name),
+ })
+);
diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts
new file mode 100644
index 00000000..bfb08079
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts
@@ -0,0 +1,34 @@
+// Example model schema from the Drizzle docs
+// https://orm.drizzle.team/docs/sql-schema-declaration
+
+import { sql } from "drizzle-orm";
+import {
+ bigint,
+ index,
+ mysqlTableCreator,
+ timestamp,
+ varchar,
+} from "drizzle-orm/mysql-core";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = mysqlTableCreator((name) => `project1_${name}`);
+
+export const posts = createTable(
+ "post",
+ {
+ id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
+ name: varchar("name", { length: 256 }),
+ createdAt: timestamp("created_at")
+ .default(sql`CURRENT_TIMESTAMP`)
+ .notNull(),
+ updatedAt: timestamp("updated_at").onUpdateNow(),
+ },
+ (example) => ({
+ nameIndex: index("name_idx").on(example.name),
+ })
+);
diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts
new file mode 100644
index 00000000..8e6f2f99
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts
@@ -0,0 +1,36 @@
+// Example model schema from the Drizzle docs
+// https://orm.drizzle.team/docs/sql-schema-declaration
+
+import { sql } from "drizzle-orm";
+import {
+ index,
+ pgTableCreator,
+ serial,
+ timestamp,
+ varchar,
+} from "drizzle-orm/pg-core";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = pgTableCreator((name) => `project1_${name}`);
+
+export const posts = createTable(
+ "post",
+ {
+ id: serial("id").primaryKey(),
+ name: varchar("name", { length: 256 }),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .default(sql`CURRENT_TIMESTAMP`)
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
+ () => new Date()
+ ),
+ },
+ (example) => ({
+ nameIndex: index("name_idx").on(example.name),
+ })
+);
diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts
new file mode 100644
index 00000000..cc74c86a
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts
@@ -0,0 +1,30 @@
+// Example model schema from the Drizzle docs
+// https://orm.drizzle.team/docs/sql-schema-declaration
+
+import { sql } from "drizzle-orm";
+import { index, int, sqliteTableCreator, text } from "drizzle-orm/sqlite-core";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = sqliteTableCreator((name) => `project1_${name}`);
+
+export const posts = createTable(
+ "post",
+ {
+ id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
+ name: text("name", { length: 256 }),
+ createdAt: int("created_at", { mode: "timestamp" })
+ .default(sql`(unixepoch())`)
+ .notNull(),
+ updatedAt: int("updated_at", { mode: "timestamp" }).$onUpdate(
+ () => new Date()
+ ),
+ },
+ (example) => ({
+ nameIndex: index("name_idx").on(example.name),
+ })
+);
diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts
new file mode 100644
index 00000000..96e9a85c
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts
@@ -0,0 +1,123 @@
+import { relations, sql } from "drizzle-orm";
+import {
+ bigint,
+ index,
+ int,
+ mysqlTableCreator,
+ primaryKey,
+ text,
+ timestamp,
+ varchar,
+} from "drizzle-orm/mysql-core";
+import { type AdapterAccount } from "next-auth/adapters";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = mysqlTableCreator((name) => `project1_${name}`);
+
+export const posts = createTable(
+ "post",
+ {
+ id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
+ name: varchar("name", { length: 256 }),
+ createdById: varchar("created_by", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ createdAt: timestamp("created_at")
+ .default(sql`CURRENT_TIMESTAMP`)
+ .notNull(),
+ updatedAt: timestamp("updated_at").onUpdateNow(),
+ },
+ (example) => ({
+ createdByIdIdx: index("created_by_idx").on(example.createdById),
+ nameIndex: index("name_idx").on(example.name),
+ })
+);
+
+export const users = createTable("user", {
+ id: varchar("id", { length: 255 })
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ name: varchar("name", { length: 255 }),
+ email: varchar("email", { length: 255 }).notNull(),
+ emailVerified: timestamp("email_verified", {
+ mode: "date",
+ fsp: 3,
+ }).default(sql`CURRENT_TIMESTAMP(3)`),
+ image: varchar("image", { length: 255 }),
+});
+
+export const usersRelations = relations(users, ({ many }) => ({
+ accounts: many(accounts),
+ sessions: many(sessions),
+}));
+
+export const accounts = createTable(
+ "account",
+ {
+ userId: varchar("user_id", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ type: varchar("type", { length: 255 })
+ .$type()
+ .notNull(),
+ provider: varchar("provider", { length: 255 }).notNull(),
+ providerAccountId: varchar("provider_account_id", {
+ length: 255,
+ }).notNull(),
+ refresh_token: text("refresh_token"),
+ access_token: text("access_token"),
+ expires_at: int("expires_at"),
+ token_type: varchar("token_type", { length: 255 }),
+ scope: varchar("scope", { length: 255 }),
+ id_token: text("id_token"),
+ session_state: varchar("session_state", { length: 255 }),
+ },
+ (account) => ({
+ compoundKey: primaryKey({
+ columns: [account.provider, account.providerAccountId],
+ }),
+ userIdIdx: index("account_user_id_idx").on(account.userId),
+ })
+);
+
+export const accountsRelations = relations(accounts, ({ one }) => ({
+ user: one(users, { fields: [accounts.userId], references: [users.id] }),
+}));
+
+export const sessions = createTable(
+ "session",
+ {
+ sessionToken: varchar("session_token", { length: 255 })
+ .notNull()
+ .primaryKey(),
+ userId: varchar("user_id", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ expires: timestamp("expires", { mode: "date" }).notNull(),
+ },
+ (session) => ({
+ userIdIdx: index("session_user_id_idx").on(session.userId),
+ })
+);
+
+export const sessionsRelations = relations(sessions, ({ one }) => ({
+ user: one(users, { fields: [sessions.userId], references: [users.id] }),
+}));
+
+export const verificationTokens = createTable(
+ "verification_token",
+ {
+ identifier: varchar("identifier", { length: 255 }).notNull(),
+ token: varchar("token", { length: 255 }).notNull(),
+ expires: timestamp("expires", { mode: "date" }).notNull(),
+ },
+ (vt) => ({
+ compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
+ })
+);
diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts
new file mode 100644
index 00000000..a0b1d72f
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts
@@ -0,0 +1,117 @@
+import { relations, sql } from "drizzle-orm";
+import {
+ bigint,
+ index,
+ int,
+ mysqlTableCreator,
+ primaryKey,
+ text,
+ timestamp,
+ varchar,
+} from "drizzle-orm/mysql-core";
+import { type AdapterAccount } from "next-auth/adapters";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = mysqlTableCreator((name) => `project1_${name}`);
+
+export const posts = createTable(
+ "post",
+ {
+ id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
+ name: varchar("name", { length: 256 }),
+ createdById: varchar("created_by", { length: 255 }).notNull(),
+ createdAt: timestamp("created_at")
+ .default(sql`CURRENT_TIMESTAMP`)
+ .notNull(),
+ updatedAt: timestamp("updated_at").onUpdateNow(),
+ },
+ (example) => ({
+ createdByIdIdx: index("created_by_idx").on(example.createdById),
+ nameIndex: index("name_idx").on(example.name),
+ })
+);
+
+export const users = createTable("user", {
+ id: varchar("id", { length: 255 })
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ name: varchar("name", { length: 255 }),
+ email: varchar("email", { length: 255 }).notNull(),
+ emailVerified: timestamp("email_verified", {
+ mode: "date",
+ fsp: 3,
+ }).default(sql`CURRENT_TIMESTAMP(3)`),
+ image: varchar("image", { length: 255 }),
+});
+
+export const usersRelations = relations(users, ({ many }) => ({
+ accounts: many(accounts),
+ sessions: many(sessions),
+}));
+
+export const accounts = createTable(
+ "account",
+ {
+ userId: varchar("user_id", { length: 255 }).notNull(),
+ type: varchar("type", { length: 255 })
+ .$type()
+ .notNull(),
+ provider: varchar("provider", { length: 255 }).notNull(),
+ providerAccountId: varchar("provider_account_id", {
+ length: 255,
+ }).notNull(),
+ refresh_token: text("refresh_token"),
+ access_token: text("access_token"),
+ expires_at: int("expires_at"),
+ token_type: varchar("token_type", { length: 255 }),
+ scope: varchar("scope", { length: 255 }),
+ id_token: text("id_token"),
+ session_state: varchar("session_state", { length: 255 }),
+ },
+ (account) => ({
+ compoundKey: primaryKey({
+ columns: [account.provider, account.providerAccountId],
+ }),
+ userIdIdx: index("accounts_user_id_idx").on(account.userId),
+ })
+);
+
+export const accountsRelations = relations(accounts, ({ one }) => ({
+ user: one(users, { fields: [accounts.userId], references: [users.id] }),
+}));
+
+export const sessions = createTable(
+ "session",
+ {
+ sessionToken: varchar("session_token", { length: 255 })
+ .notNull()
+ .primaryKey(),
+ userId: varchar("user_id", { length: 255 }).notNull(),
+ expires: timestamp("expires", { mode: "date" }).notNull(),
+ },
+ (session) => ({
+ userIdIdx: index("session_user_id_idx").on(session.userId),
+ })
+);
+
+export const sessionsRelations = relations(sessions, ({ one }) => ({
+ user: one(users, { fields: [sessions.userId], references: [users.id] }),
+}));
+
+export const verificationTokens = createTable(
+ "verification_token",
+ {
+ identifier: varchar("identifier", { length: 255 }).notNull(),
+ token: varchar("token", { length: 255 }).notNull(),
+ expires: timestamp("expires", { mode: "date" }).notNull(),
+ },
+ (vt) => ({
+ compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
+ })
+);
diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts
new file mode 100644
index 00000000..5ce3f9c2
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts
@@ -0,0 +1,130 @@
+import { relations, sql } from "drizzle-orm";
+import {
+ index,
+ integer,
+ pgTableCreator,
+ primaryKey,
+ serial,
+ text,
+ timestamp,
+ varchar,
+} from "drizzle-orm/pg-core";
+import { type AdapterAccount } from "next-auth/adapters";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = pgTableCreator((name) => `project1_${name}`);
+
+export const posts = createTable(
+ "post",
+ {
+ id: serial("id").primaryKey(),
+ name: varchar("name", { length: 256 }),
+ createdById: varchar("created_by", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .default(sql`CURRENT_TIMESTAMP`)
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
+ () => new Date()
+ ),
+ },
+ (example) => ({
+ createdByIdIdx: index("created_by_idx").on(example.createdById),
+ nameIndex: index("name_idx").on(example.name),
+ })
+);
+
+export const users = createTable("user", {
+ id: varchar("id", { length: 255 })
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ name: varchar("name", { length: 255 }),
+ email: varchar("email", { length: 255 }).notNull(),
+ emailVerified: timestamp("email_verified", {
+ mode: "date",
+ withTimezone: true,
+ }).default(sql`CURRENT_TIMESTAMP`),
+ image: varchar("image", { length: 255 }),
+});
+
+export const usersRelations = relations(users, ({ many }) => ({
+ accounts: many(accounts),
+}));
+
+export const accounts = createTable(
+ "account",
+ {
+ userId: varchar("user_id", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ type: varchar("type", { length: 255 })
+ .$type()
+ .notNull(),
+ provider: varchar("provider", { length: 255 }).notNull(),
+ providerAccountId: varchar("provider_account_id", {
+ length: 255,
+ }).notNull(),
+ refresh_token: text("refresh_token"),
+ access_token: text("access_token"),
+ expires_at: integer("expires_at"),
+ token_type: varchar("token_type", { length: 255 }),
+ scope: varchar("scope", { length: 255 }),
+ id_token: text("id_token"),
+ session_state: varchar("session_state", { length: 255 }),
+ },
+ (account) => ({
+ compoundKey: primaryKey({
+ columns: [account.provider, account.providerAccountId],
+ }),
+ userIdIdx: index("account_user_id_idx").on(account.userId),
+ })
+);
+
+export const accountsRelations = relations(accounts, ({ one }) => ({
+ user: one(users, { fields: [accounts.userId], references: [users.id] }),
+}));
+
+export const sessions = createTable(
+ "session",
+ {
+ sessionToken: varchar("session_token", { length: 255 })
+ .notNull()
+ .primaryKey(),
+ userId: varchar("user_id", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ expires: timestamp("expires", {
+ mode: "date",
+ withTimezone: true,
+ }).notNull(),
+ },
+ (session) => ({
+ userIdIdx: index("session_user_id_idx").on(session.userId),
+ })
+);
+
+export const sessionsRelations = relations(sessions, ({ one }) => ({
+ user: one(users, { fields: [sessions.userId], references: [users.id] }),
+}));
+
+export const verificationTokens = createTable(
+ "verification_token",
+ {
+ identifier: varchar("identifier", { length: 255 }).notNull(),
+ token: varchar("token", { length: 255 }).notNull(),
+ expires: timestamp("expires", {
+ mode: "date",
+ withTimezone: true,
+ }).notNull(),
+ },
+ (vt) => ({
+ compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
+ })
+);
diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts
new file mode 100644
index 00000000..12ee2901
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts
@@ -0,0 +1,116 @@
+import { relations, sql } from "drizzle-orm";
+import {
+ index,
+ int,
+ primaryKey,
+ sqliteTableCreator,
+ text,
+} from "drizzle-orm/sqlite-core";
+import { type AdapterAccount } from "next-auth/adapters";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = sqliteTableCreator((name) => `project1_${name}`);
+
+export const posts = createTable(
+ "post",
+ {
+ id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
+ name: text("name", { length: 256 }),
+ createdById: text("created_by", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ createdAt: int("created_at", { mode: "timestamp" })
+ .default(sql`(unixepoch())`)
+ .notNull(),
+ updatedAt: int("updatedAt", { mode: "timestamp" }).$onUpdate(
+ () => new Date()
+ ),
+ },
+ (example) => ({
+ createdByIdIdx: index("created_by_idx").on(example.createdById),
+ nameIndex: index("name_idx").on(example.name),
+ })
+);
+
+export const users = createTable("user", {
+ id: text("id", { length: 255 })
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ name: text("name", { length: 255 }),
+ email: text("email", { length: 255 }).notNull(),
+ emailVerified: int("email_verified", {
+ mode: "timestamp",
+ }).default(sql`(unixepoch())`),
+ image: text("image", { length: 255 }),
+});
+
+export const usersRelations = relations(users, ({ many }) => ({
+ accounts: many(accounts),
+}));
+
+export const accounts = createTable(
+ "account",
+ {
+ userId: text("user_id", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ type: text("type", { length: 255 })
+ .$type()
+ .notNull(),
+ provider: text("provider", { length: 255 }).notNull(),
+ providerAccountId: text("provider_account_id", { length: 255 }).notNull(),
+ refresh_token: text("refresh_token"),
+ access_token: text("access_token"),
+ expires_at: int("expires_at"),
+ token_type: text("token_type", { length: 255 }),
+ scope: text("scope", { length: 255 }),
+ id_token: text("id_token"),
+ session_state: text("session_state", { length: 255 }),
+ },
+ (account) => ({
+ compoundKey: primaryKey({
+ columns: [account.provider, account.providerAccountId],
+ }),
+ userIdIdx: index("account_user_id_idx").on(account.userId),
+ })
+);
+
+export const accountsRelations = relations(accounts, ({ one }) => ({
+ user: one(users, { fields: [accounts.userId], references: [users.id] }),
+}));
+
+export const sessions = createTable(
+ "session",
+ {
+ sessionToken: text("session_token", { length: 255 }).notNull().primaryKey(),
+ userId: text("userId", { length: 255 })
+ .notNull()
+ .references(() => users.id),
+ expires: int("expires", { mode: "timestamp" }).notNull(),
+ },
+ (session) => ({
+ userIdIdx: index("session_userId_idx").on(session.userId),
+ })
+);
+
+export const sessionsRelations = relations(sessions, ({ one }) => ({
+ user: one(users, { fields: [sessions.userId], references: [users.id] }),
+}));
+
+export const verificationTokens = createTable(
+ "verification_token",
+ {
+ identifier: text("identifier", { length: 255 }).notNull(),
+ token: text("token", { length: 255 }).notNull(),
+ expires: int("expires", { mode: "timestamp" }).notNull(),
+ },
+ (vt) => ({
+ compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
+ })
+);
diff --git a/packages/cli-old/template/extras/src/server/next-auth/base.ts b/packages/cli-old/template/extras/src/server/next-auth/base.ts
new file mode 100644
index 00000000..ac29d61f
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/next-auth/base.ts
@@ -0,0 +1,111 @@
+
+
+
+
+import { env } from "@/config/env";
+import { OttoAdapter } from "@proofkit/fmdapi";
+import NextAuth, { type DefaultSession } from "next-auth";
+import { FilemakerAdapter } from "next-auth-adapter-filemaker";
+import { type Provider } from "next-auth/providers";
+import Credentials from "next-auth/providers/credentials";
+import { z } from "zod/v4";
+
+import { verifyPassword } from "./password";
+
+export const fmAdapter = FilemakerAdapter({
+ adapter: new OttoAdapter({
+ auth: { apiKey: env.OTTO_API_KEY },
+ db: env.FM_DATABASE,
+ server: env.FM_SERVER,
+ }),
+});
+
+/**
+ * Module augmentation for `next-auth` types. Alldows us to add custom properties to the `session`
+ * object and keep type safety.
+ *
+ * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
+ */
+declare module "next-auth" {
+ interface Session extends DefaultSession {
+ user: {
+ id: string;
+ // ...other properties
+ // role: UserRole;
+ } & DefaultSession["user"];
+ }
+
+ // interface User {
+ // // ...other properties
+ // // role: UserRole;
+ // }
+}
+
+const signInSchema = z.object({
+ email: z.string().email(),
+ password: z.string(),
+});
+
+const providers: Provider[] = [
+ Credentials({
+ credentials: {
+ email: { label: "Email", type: "email" },
+ password: { label: "Password", type: "password" },
+ },
+ authorize: async (credentials) => {
+ const parsed = signInSchema.safeParse(credentials);
+ if (!parsed.success) {
+ return null;
+ }
+
+ const { email, password } = parsed.data;
+
+ try {
+ // logic to verify if the user exists with the password hash
+ const userResponse =
+ await fmAdapter.typedClients.userWithPasswordHash.findOne({
+ query: { email: `==${email.replace("@", "\\@")}` },
+ });
+ const { passwordHash, ...userData } = userResponse.data.fieldData;
+ const isValid = await verifyPassword(password, passwordHash);
+ if (!isValid) return null;
+
+ return userData;
+ } catch (error) {
+ console.log("error", error);
+ throw new Error("User not found.");
+ }
+ },
+ }),
+];
+
+export const providerMap = providers
+ .map((provider) => {
+ if (typeof provider === "function") {
+ const providerData = provider();
+ return { id: providerData.id, name: providerData.name };
+ } else {
+ return { id: provider.id, name: provider.name };
+ }
+ })
+ .filter((provider) => provider.id !== "credentials");
+
+export const { auth, handlers, signIn, signOut } = NextAuth({
+ pages: {
+ signIn: "/auth/signin",
+ newUser: "/auth/signup",
+ error: "/auth/signin",
+ },
+ callbacks: {
+ session: ({ session, token }) => ({
+ ...session,
+ user: {
+ ...session.user,
+ id: token.sub,
+ },
+ }),
+ },
+ adapter: fmAdapter.Adapter,
+ session: { strategy: "jwt" },
+ providers,
+});
diff --git a/packages/cli-old/template/extras/src/server/next-auth/password.ts b/packages/cli-old/template/extras/src/server/next-auth/password.ts
new file mode 100644
index 00000000..a82f34c6
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/next-auth/password.ts
@@ -0,0 +1,13 @@
+export async function saltAndHashPassword(password: string): Promise {
+ const bcrypt = await import("bcrypt");
+ const saltRounds = 12;
+ return bcrypt.hash(password, saltRounds);
+}
+
+export async function verifyPassword(
+ plainTextPassword: string,
+ hashedPassword: string
+): Promise {
+ const bcrypt = await import("bcrypt");
+ return bcrypt.compare(plainTextPassword, hashedPassword);
+}
diff --git a/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts b/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts
new file mode 100644
index 00000000..6e9281d1
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts
@@ -0,0 +1,83 @@
+import { DrizzleAdapter } from "@auth/drizzle-adapter";
+import {
+ getServerSession,
+ type DefaultSession,
+ type NextAuthOptions,
+} from "next-auth";
+import { type Adapter } from "next-auth/adapters";
+import DiscordProvider from "next-auth/providers/discord";
+
+import { env } from "~/env";
+import { db } from "~/server/db";
+import {
+ accounts,
+ sessions,
+ users,
+ verificationTokens,
+} from "~/server/db/schema";
+
+/**
+ * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
+ * object and keep type safety.
+ *
+ * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
+ */
+declare module "next-auth" {
+ interface Session extends DefaultSession {
+ user: {
+ id: string;
+ // ...other properties
+ // role: UserRole;
+ } & DefaultSession["user"];
+ }
+
+ // interface User {
+ // // ...other properties
+ // // role: UserRole;
+ // }
+}
+
+/**
+ * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
+ *
+ * @see https://next-auth.js.org/configuration/options
+ */
+export const authOptions: NextAuthOptions = {
+ callbacks: {
+ session: ({ session, user }) => ({
+ ...session,
+ user: {
+ ...session.user,
+ id: user.id,
+ },
+ }),
+ },
+ adapter: DrizzleAdapter(db, {
+ usersTable: users,
+ accountsTable: accounts,
+ sessionsTable: sessions,
+ verificationTokensTable: verificationTokens,
+ }) as Adapter,
+ providers: [
+ DiscordProvider({
+ clientId: env.DISCORD_CLIENT_ID,
+ clientSecret: env.DISCORD_CLIENT_SECRET,
+ }),
+ /**
+ * ...add more providers here.
+ *
+ * Most other providers require a bit more work than the Discord provider. For example, the
+ * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
+ * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
+ *
+ * @see https://next-auth.js.org/providers/github
+ */
+ ],
+};
+
+/**
+ * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
+ *
+ * @see https://next-auth.js.org/configuration/nextjs
+ */
+export const getServerAuthSession = () => getServerSession(authOptions);
diff --git a/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts b/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts
new file mode 100644
index 00000000..117984c9
--- /dev/null
+++ b/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts
@@ -0,0 +1,72 @@
+import { PrismaAdapter } from "@auth/prisma-adapter";
+import {
+ getServerSession,
+ type DefaultSession,
+ type NextAuthOptions,
+} from "next-auth";
+import { type Adapter } from "next-auth/adapters";
+import DiscordProvider from "next-auth/providers/discord";
+
+import { env } from "~/env";
+import { db } from "~/server/db";
+
+/**
+ * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
+ * object and keep type safety.
+ *
+ * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
+ */
+declare module "next-auth" {
+ interface Session extends DefaultSession {
+ user: {
+ id: string;
+ // ...other properties
+ // role: UserRole;
+ } & DefaultSession["user"];
+ }
+
+ // interface User {
+ // // ...other properties
+ // // role: UserRole;
+ // }
+}
+
+/**
+ * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
+ *
+ * @see https://next-auth.js.org/configuration/options
+ */
+export const authOptions: NextAuthOptions = {
+ callbacks: {
+ session: ({ session, user }) => ({
+ ...session,
+ user: {
+ ...session.user,
+ id: user.id,
+ },
+ }),
+ },
+ adapter: PrismaAdapter(db) as Adapter,
+ providers: [
+ DiscordProvider({
+ clientId: env.DISCORD_CLIENT_ID,
+ clientSecret: env.DISCORD_CLIENT_SECRET,
+ }),
+ /**
+ * ...add more providers here.
+ *
+ * Most other providers require a bit more work than the Discord provider. For example, the
+ * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
+ * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
+ *
+ * @see https://next-auth.js.org/providers/github
+ */
+ ],
+};
+
+/**
+ * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
+ *
+ * @see https://next-auth.js.org/configuration/nextjs
+ */
+export const getServerAuthSession = () => getServerSession(authOptions);
diff --git a/packages/cli-old/template/extras/src/trpc/query-client.ts b/packages/cli-old/template/extras/src/trpc/query-client.ts
new file mode 100644
index 00000000..bda64397
--- /dev/null
+++ b/packages/cli-old/template/extras/src/trpc/query-client.ts
@@ -0,0 +1,25 @@
+import {
+ defaultShouldDehydrateQuery,
+ QueryClient,
+} from "@tanstack/react-query";
+import SuperJSON from "superjson";
+
+export const createQueryClient = () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ // With SSR, we usually want to set some default staleTime
+ // above 0 to avoid refetching immediately on the client
+ staleTime: 30 * 1000,
+ },
+ dehydrate: {
+ serializeData: SuperJSON.serialize,
+ shouldDehydrateQuery: (query) =>
+ defaultShouldDehydrateQuery(query) ||
+ query.state.status === "pending",
+ },
+ hydrate: {
+ deserializeData: SuperJSON.deserialize,
+ },
+ },
+ });
diff --git a/packages/cli-old/template/extras/src/trpc/react.tsx b/packages/cli-old/template/extras/src/trpc/react.tsx
new file mode 100644
index 00000000..8c0521a7
--- /dev/null
+++ b/packages/cli-old/template/extras/src/trpc/react.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
+import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
+import { createTRPCReact } from "@trpc/react-query";
+import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
+import { useState } from "react";
+import SuperJSON from "superjson";
+
+import { type AppRouter } from "~/server/api/root";
+import { createQueryClient } from "./query-client";
+
+let clientQueryClientSingleton: QueryClient | undefined = undefined;
+const getQueryClient = () => {
+ if (typeof window === "undefined") {
+ // Server: always make a new query client
+ return createQueryClient();
+ }
+ // Browser: use singleton pattern to keep the same query client
+ return (clientQueryClientSingleton ??= createQueryClient());
+};
+
+export const api = createTRPCReact();
+
+/**
+ * Inference helper for inputs.
+ *
+ * @example type HelloInput = RouterInputs['example']['hello']
+ */
+export type RouterInputs = inferRouterInputs;
+
+/**
+ * Inference helper for outputs.
+ *
+ * @example type HelloOutput = RouterOutputs['example']['hello']
+ */
+export type RouterOutputs = inferRouterOutputs;
+
+export function TRPCReactProvider(props: { children: React.ReactNode }) {
+ const queryClient = getQueryClient();
+
+ const [trpcClient] = useState(() =>
+ api.createClient({
+ links: [
+ loggerLink({
+ enabled: (op) =>
+ process.env.NODE_ENV === "development" ||
+ (op.direction === "down" && op.result instanceof Error),
+ }),
+ unstable_httpBatchStreamLink({
+ transformer: SuperJSON,
+ url: getBaseUrl() + "/api/trpc",
+ headers: () => {
+ const headers = new Headers();
+ headers.set("x-trpc-source", "nextjs-react");
+ return headers;
+ },
+ }),
+ ],
+ })
+ );
+
+ return (
+
+
+ {props.children}
+
+
+ );
+}
+
+function getBaseUrl() {
+ if (typeof window !== "undefined") return window.location.origin;
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
+ return `http://localhost:${process.env.PORT ?? 3000}`;
+}
diff --git a/packages/cli-old/template/extras/src/trpc/server.ts b/packages/cli-old/template/extras/src/trpc/server.ts
new file mode 100644
index 00000000..59300a63
--- /dev/null
+++ b/packages/cli-old/template/extras/src/trpc/server.ts
@@ -0,0 +1,30 @@
+import "server-only";
+
+import { createHydrationHelpers } from "@trpc/react-query/rsc";
+import { headers } from "next/headers";
+import { cache } from "react";
+
+import { createCaller, type AppRouter } from "~/server/api/root";
+import { createTRPCContext } from "~/server/api/trpc";
+import { createQueryClient } from "./query-client";
+
+/**
+ * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
+ * handling a tRPC call from a React Server Component.
+ */
+const createContext = cache(() => {
+ const heads = new Headers(headers());
+ heads.set("x-trpc-source", "rsc");
+
+ return createTRPCContext({
+ headers: heads,
+ });
+});
+
+const getQueryClient = cache(createQueryClient);
+const caller = createCaller(createContext);
+
+export const { trpc: api, HydrateClient } = createHydrationHelpers(
+ caller,
+ getQueryClient
+);
diff --git a/packages/cli-old/template/extras/src/utils/api.ts b/packages/cli-old/template/extras/src/utils/api.ts
new file mode 100644
index 00000000..0f03d307
--- /dev/null
+++ b/packages/cli-old/template/extras/src/utils/api.ts
@@ -0,0 +1,68 @@
+/**
+ * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which
+ * contains the Next.js App-wrapper, as well as your type-safe React Query hooks.
+ *
+ * We also create a few inference helpers for input and output types.
+ */
+import { httpBatchLink, loggerLink } from "@trpc/client";
+import { createTRPCNext } from "@trpc/next";
+import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
+import superjson from "superjson";
+
+import { type AppRouter } from "~/server/api/root";
+
+const getBaseUrl = () => {
+ if (typeof window !== "undefined") return ""; // browser should use relative url
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
+ return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
+};
+
+/** A set of type-safe react-query hooks for your tRPC API. */
+export const api = createTRPCNext({
+ config() {
+ return {
+ /**
+ * Links used to determine request flow from client to server.
+ *
+ * @see https://trpc.io/docs/links
+ */
+ links: [
+ loggerLink({
+ enabled: (opts) =>
+ process.env.NODE_ENV === "development" ||
+ (opts.direction === "down" && opts.result instanceof Error),
+ }),
+ httpBatchLink({
+ /**
+ * Transformer used for data de-serialization from the server.
+ *
+ * @see https://trpc.io/docs/data-transformers
+ */
+ transformer: superjson,
+ url: `${getBaseUrl()}/api/trpc`,
+ }),
+ ],
+ };
+ },
+ /**
+ * Whether tRPC should await queries when server rendering pages.
+ *
+ * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
+ */
+ ssr: false,
+ transformer: superjson,
+});
+
+/**
+ * Inference helper for inputs.
+ *
+ * @example type HelloInput = RouterInputs['example']['hello']
+ */
+export type RouterInputs = inferRouterInputs;
+
+/**
+ * Inference helper for outputs.
+ *
+ * @example type HelloOutput = RouterOutputs['example']['hello']
+ */
+export type RouterOutputs = inferRouterOutputs;
diff --git a/packages/cli-old/template/extras/start-database/mysql.sh b/packages/cli-old/template/extras/start-database/mysql.sh
new file mode 100755
index 00000000..268df5cc
--- /dev/null
+++ b/packages/cli-old/template/extras/start-database/mysql.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+# Use this script to start a docker container for a local development database
+
+# TO RUN ON WINDOWS:
+# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
+# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
+# 3. Open WSL - `wsl`
+# 4. Run this script - `./start-database.sh`
+
+# On Linux and macOS you can run this script directly - `./start-database.sh`
+
+DB_CONTAINER_NAME="project1-mysql"
+
+if ! [ -x "$(command -v docker)" ]; then
+ echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/"
+ exit 1
+fi
+
+if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then
+ echo "Database container '$DB_CONTAINER_NAME' already running"
+ exit 0
+fi
+
+if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
+ docker start "$DB_CONTAINER_NAME"
+ echo "Existing database container '$DB_CONTAINER_NAME' started"
+ exit 0
+fi
+
+# import env variables from .env
+set -a
+source .env
+
+DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
+DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
+
+if [ "$DB_PASSWORD" == "password" ]; then
+ echo "You are using the default database password"
+ read -p "Should we generate a random password for you? [y/N]: " -r REPLY
+ if ! [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Please change the default password in the .env file and try again"
+ exit 1
+ fi
+ # Generate a random URL-safe password
+ DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
+ sed -i -e "s#:password@#:$DB_PASSWORD@#" .env
+fi
+
+docker run -d \
+ --name $DB_CONTAINER_NAME \
+ -e MYSQL_ROOT_PASSWORD="$DB_PASSWORD" \
+ -e MYSQL_DATABASE=project1 \
+ -p "$DB_PORT":3306 \
+ docker.io/mysql && echo "Database container '$DB_CONTAINER_NAME' was successfully created"
diff --git a/packages/cli-old/template/extras/start-database/postgres.sh b/packages/cli-old/template/extras/start-database/postgres.sh
new file mode 100755
index 00000000..11fb2042
--- /dev/null
+++ b/packages/cli-old/template/extras/start-database/postgres.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# Use this script to start a docker container for a local development database
+
+# TO RUN ON WINDOWS:
+# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
+# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
+# 3. Open WSL - `wsl`
+# 4. Run this script - `./start-database.sh`
+
+# On Linux and macOS you can run this script directly - `./start-database.sh`
+
+DB_CONTAINER_NAME="project1-postgres"
+
+if ! [ -x "$(command -v docker)" ]; then
+ echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/"
+ exit 1
+fi
+
+if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then
+ echo "Database container '$DB_CONTAINER_NAME' already running"
+ exit 0
+fi
+
+if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
+ docker start "$DB_CONTAINER_NAME"
+ echo "Existing database container '$DB_CONTAINER_NAME' started"
+ exit 0
+fi
+
+# import env variables from .env
+set -a
+source .env
+
+DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
+DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
+
+if [ "$DB_PASSWORD" = "password" ]; then
+ echo "You are using the default database password"
+ read -p "Should we generate a random password for you? [y/N]: " -r REPLY
+ if ! [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Please change the default password in the .env file and try again"
+ exit 1
+ fi
+ # Generate a random URL-safe password
+ DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
+ sed -i -e "s#:password@#:$DB_PASSWORD@#" .env
+fi
+
+docker run -d \
+ --name $DB_CONTAINER_NAME \
+ -e POSTGRES_USER="postgres" \
+ -e POSTGRES_PASSWORD="$DB_PASSWORD" \
+ -e POSTGRES_DB=project1 \
+ -p "$DB_PORT":5432 \
+ docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created"
diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/de.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/de.xml
new file mode 100644
index 00000000..5df205c4
--- /dev/null
+++ b/packages/cli-old/template/fm-addon/ProofKitAuth/de.xml
@@ -0,0 +1,518 @@
+
+
+ com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.basetable.proofkit_auth_sessions
+
+
+
+ com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.basetable.proofkit_auth_email_verification
+
+
+
+ com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.basetable.proofkit_auth_password_reset
+
+
+
+ com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.basetable.proofkit_auth_users
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.basetable.field.proofkit_auth_email_verification::code
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_email_verification::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_email_verification::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.basetable.field.proofkit_auth_email_verification::expires_at
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_email_verification::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.basetable.field.proofkit_auth_password_reset::code
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_password_reset::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_password_reset::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.basetable.field.proofkit_auth_password_reset::email_verified
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.basetable.field.proofkit_auth_password_reset::expires_at
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_password_reset::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_sessions::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.basetable.field.proofkit_auth_sessions::expiresAt
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_sessions::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_users::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_users::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.basetable.field.proofkit_auth_users::emailVerified
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.basetable.field.proofkit_auth_users::password_hash
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.basetable.field.proofkit_auth_users::username
+
+
+
+ com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3
+ https://proofkit.dev/auth/fm-addon/
+ https://proofkit.dev/auth/fm-addon/
+ com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/
+
+
+
+ com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F
+ https://proofkit.dev
+ https://proofkit.dev
+ com.fmi.calculation.text.https://proofkit.dev
+
+
+
+ com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.layout.proofkit_auth_sessions
+
+
+
+ com.fmi.layout.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.layout.proofkit_auth_email_verification
+
+
+
+ com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.layout.proofkit_auth_password_reset
+
+
+
+ com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.layout.proofkit_auth_users
+
+
+
+ com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.layoutobject.text.code
+
+
+
+ com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346
+ Session
+ Session
+ com.fmi.layoutobject.text.Session
+
+
+
+ com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE
+ User
+ User
+ com.fmi.layoutobject.text.User
+
+
+
+ com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC
+ This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+ This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+ com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+
+
+
+ com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.layoutobject.text.id
+
+
+
+ com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.layoutobject.text.email
+
+
+
+ com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.layoutobject.text.expiresAt
+
+
+
+ com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.layoutobject.text.email_verified
+
+
+
+ com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499
+ Password Reset
+ Password Reset
+ com.fmi.layoutobject.text.Password Reset
+
+
+
+ com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.layoutobject.text.emailVerified
+
+
+
+ com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.layoutobject.text.expires_at
+
+
+
+ com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.layoutobject.text.password_hash
+
+
+
+ com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A
+ Related User
+ Related User
+ com.fmi.layoutobject.text.Related User
+
+
+
+ com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630
+ It's safe to delete this record if the verification has expired
+ It's safe to delete this record if the verification has expired
+ com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired
+
+
+
+ com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E
+ This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+ This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+ com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+
+
+
+ com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233
+ time in milliseconds
+ time in milliseconds
+ com.fmi.layoutobject.text.time in milliseconds
+
+
+
+ com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.layoutobject.text.id_user
+
+
+
+ com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D
+ When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+ When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+ com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+
+
+
+ com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.layoutobject.text.username
+
+
+
+ com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15
+ When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+ When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+ com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+
+
+
+ com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF
+ Email Verifications
+ Email Verifications
+ com.fmi.layoutobject.text.Email Verifications
+
+
+
+ com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932
+ Learn more at proofkit.dev
+ Learn more at proofkit.dev
+ com.fmi.layoutobject.text.Learn more at proofkit.dev
+
+
+
+ com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.tableoccurrence.proofkit_auth_sessions
+
+
+
+ com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.tableoccurrence.proofkit_auth_email_verification
+
+
+
+ com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.tableoccurrence.proofkit_auth_password_reset
+
+
+
+ com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.tableoccurrence.proofkit_auth_users
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_users::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_users::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.tableoccurrence.field.proofkit_auth_users::username
+
diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/en.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/en.xml
new file mode 100644
index 00000000..5df205c4
--- /dev/null
+++ b/packages/cli-old/template/fm-addon/ProofKitAuth/en.xml
@@ -0,0 +1,518 @@
+
+
+ com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.basetable.proofkit_auth_sessions
+
+
+
+ com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.basetable.proofkit_auth_email_verification
+
+
+
+ com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.basetable.proofkit_auth_password_reset
+
+
+
+ com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.basetable.proofkit_auth_users
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.basetable.field.proofkit_auth_email_verification::code
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_email_verification::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_email_verification::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.basetable.field.proofkit_auth_email_verification::expires_at
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_email_verification::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.basetable.field.proofkit_auth_password_reset::code
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_password_reset::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_password_reset::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.basetable.field.proofkit_auth_password_reset::email_verified
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.basetable.field.proofkit_auth_password_reset::expires_at
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_password_reset::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_sessions::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.basetable.field.proofkit_auth_sessions::expiresAt
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_sessions::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_users::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_users::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.basetable.field.proofkit_auth_users::emailVerified
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.basetable.field.proofkit_auth_users::password_hash
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.basetable.field.proofkit_auth_users::username
+
+
+
+ com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3
+ https://proofkit.dev/auth/fm-addon/
+ https://proofkit.dev/auth/fm-addon/
+ com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/
+
+
+
+ com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F
+ https://proofkit.dev
+ https://proofkit.dev
+ com.fmi.calculation.text.https://proofkit.dev
+
+
+
+ com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.layout.proofkit_auth_sessions
+
+
+
+ com.fmi.layout.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.layout.proofkit_auth_email_verification
+
+
+
+ com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.layout.proofkit_auth_password_reset
+
+
+
+ com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.layout.proofkit_auth_users
+
+
+
+ com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.layoutobject.text.code
+
+
+
+ com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346
+ Session
+ Session
+ com.fmi.layoutobject.text.Session
+
+
+
+ com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE
+ User
+ User
+ com.fmi.layoutobject.text.User
+
+
+
+ com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC
+ This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+ This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+ com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+
+
+
+ com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.layoutobject.text.id
+
+
+
+ com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.layoutobject.text.email
+
+
+
+ com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.layoutobject.text.expiresAt
+
+
+
+ com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.layoutobject.text.email_verified
+
+
+
+ com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499
+ Password Reset
+ Password Reset
+ com.fmi.layoutobject.text.Password Reset
+
+
+
+ com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.layoutobject.text.emailVerified
+
+
+
+ com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.layoutobject.text.expires_at
+
+
+
+ com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.layoutobject.text.password_hash
+
+
+
+ com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A
+ Related User
+ Related User
+ com.fmi.layoutobject.text.Related User
+
+
+
+ com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630
+ It's safe to delete this record if the verification has expired
+ It's safe to delete this record if the verification has expired
+ com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired
+
+
+
+ com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E
+ This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+ This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+ com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+
+
+
+ com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233
+ time in milliseconds
+ time in milliseconds
+ com.fmi.layoutobject.text.time in milliseconds
+
+
+
+ com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.layoutobject.text.id_user
+
+
+
+ com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D
+ When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+ When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+ com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+
+
+
+ com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.layoutobject.text.username
+
+
+
+ com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15
+ When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+ When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+ com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+
+
+
+ com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF
+ Email Verifications
+ Email Verifications
+ com.fmi.layoutobject.text.Email Verifications
+
+
+
+ com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932
+ Learn more at proofkit.dev
+ Learn more at proofkit.dev
+ com.fmi.layoutobject.text.Learn more at proofkit.dev
+
+
+
+ com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.tableoccurrence.proofkit_auth_sessions
+
+
+
+ com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.tableoccurrence.proofkit_auth_email_verification
+
+
+
+ com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.tableoccurrence.proofkit_auth_password_reset
+
+
+
+ com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.tableoccurrence.proofkit_auth_users
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_users::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_users::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.tableoccurrence.field.proofkit_auth_users::username
+
diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/es.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/es.xml
new file mode 100644
index 00000000..5df205c4
--- /dev/null
+++ b/packages/cli-old/template/fm-addon/ProofKitAuth/es.xml
@@ -0,0 +1,518 @@
+
+
+ com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.basetable.proofkit_auth_sessions
+
+
+
+ com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.basetable.proofkit_auth_email_verification
+
+
+
+ com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.basetable.proofkit_auth_password_reset
+
+
+
+ com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.basetable.proofkit_auth_users
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.basetable.field.proofkit_auth_email_verification::code
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_email_verification::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_email_verification::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.basetable.field.proofkit_auth_email_verification::expires_at
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_email_verification::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.basetable.field.proofkit_auth_password_reset::code
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_password_reset::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_password_reset::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.basetable.field.proofkit_auth_password_reset::email_verified
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.basetable.field.proofkit_auth_password_reset::expires_at
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_password_reset::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_sessions::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.basetable.field.proofkit_auth_sessions::expiresAt
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_sessions::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_users::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_users::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.basetable.field.proofkit_auth_users::emailVerified
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.basetable.field.proofkit_auth_users::password_hash
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.basetable.field.proofkit_auth_users::username
+
+
+
+ com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3
+ https://proofkit.dev/auth/fm-addon/
+ https://proofkit.dev/auth/fm-addon/
+ com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/
+
+
+
+ com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F
+ https://proofkit.dev
+ https://proofkit.dev
+ com.fmi.calculation.text.https://proofkit.dev
+
+
+
+ com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.layout.proofkit_auth_sessions
+
+
+
+ com.fmi.layout.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.layout.proofkit_auth_email_verification
+
+
+
+ com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.layout.proofkit_auth_password_reset
+
+
+
+ com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.layout.proofkit_auth_users
+
+
+
+ com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.layoutobject.text.code
+
+
+
+ com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346
+ Session
+ Session
+ com.fmi.layoutobject.text.Session
+
+
+
+ com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE
+ User
+ User
+ com.fmi.layoutobject.text.User
+
+
+
+ com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC
+ This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+ This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+ com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+
+
+
+ com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.layoutobject.text.id
+
+
+
+ com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.layoutobject.text.email
+
+
+
+ com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.layoutobject.text.expiresAt
+
+
+
+ com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.layoutobject.text.email_verified
+
+
+
+ com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499
+ Password Reset
+ Password Reset
+ com.fmi.layoutobject.text.Password Reset
+
+
+
+ com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.layoutobject.text.emailVerified
+
+
+
+ com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.layoutobject.text.expires_at
+
+
+
+ com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.layoutobject.text.password_hash
+
+
+
+ com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A
+ Related User
+ Related User
+ com.fmi.layoutobject.text.Related User
+
+
+
+ com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630
+ It's safe to delete this record if the verification has expired
+ It's safe to delete this record if the verification has expired
+ com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired
+
+
+
+ com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E
+ This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+ This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+ com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+
+
+
+ com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233
+ time in milliseconds
+ time in milliseconds
+ com.fmi.layoutobject.text.time in milliseconds
+
+
+
+ com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.layoutobject.text.id_user
+
+
+
+ com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D
+ When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+ When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+ com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+
+
+
+ com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.layoutobject.text.username
+
+
+
+ com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15
+ When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+ When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+ com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+
+
+
+ com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF
+ Email Verifications
+ Email Verifications
+ com.fmi.layoutobject.text.Email Verifications
+
+
+
+ com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932
+ Learn more at proofkit.dev
+ Learn more at proofkit.dev
+ com.fmi.layoutobject.text.Learn more at proofkit.dev
+
+
+
+ com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.tableoccurrence.proofkit_auth_sessions
+
+
+
+ com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.tableoccurrence.proofkit_auth_email_verification
+
+
+
+ com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.tableoccurrence.proofkit_auth_password_reset
+
+
+
+ com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.tableoccurrence.proofkit_auth_users
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_users::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_users::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.tableoccurrence.field.proofkit_auth_users::username
+
diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/fr.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/fr.xml
new file mode 100644
index 00000000..5df205c4
--- /dev/null
+++ b/packages/cli-old/template/fm-addon/ProofKitAuth/fr.xml
@@ -0,0 +1,518 @@
+
+
+ com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.basetable.proofkit_auth_sessions
+
+
+
+ com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.basetable.proofkit_auth_email_verification
+
+
+
+ com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.basetable.proofkit_auth_password_reset
+
+
+
+ com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.basetable.proofkit_auth_users
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.basetable.field.proofkit_auth_email_verification::code
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_email_verification::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_email_verification::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.basetable.field.proofkit_auth_email_verification::expires_at
+
+
+
+ com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_email_verification::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.basetable.field.proofkit_auth_password_reset::code
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_password_reset::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_password_reset::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.basetable.field.proofkit_auth_password_reset::email_verified
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.basetable.field.proofkit_auth_password_reset::expires_at
+
+
+
+ com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_password_reset::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_sessions::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.basetable.field.proofkit_auth_sessions::expiresAt
+
+
+
+ com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.basetable.field.proofkit_auth_sessions::id_user
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.basetable.field.proofkit_auth_users::id
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.basetable.field.proofkit_auth_users::email
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.basetable.field.proofkit_auth_users::emailVerified
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.basetable.field.proofkit_auth_users::password_hash
+
+
+
+ com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.basetable.field.proofkit_auth_users::username
+
+
+
+ com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3
+ https://proofkit.dev/auth/fm-addon/
+ https://proofkit.dev/auth/fm-addon/
+ com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/
+
+
+
+ com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F
+ https://proofkit.dev
+ https://proofkit.dev
+ com.fmi.calculation.text.https://proofkit.dev
+
+
+
+ com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.layout.proofkit_auth_sessions
+
+
+
+ com.fmi.layout.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.layout.proofkit_auth_email_verification
+
+
+
+ com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.layout.proofkit_auth_password_reset
+
+
+
+ com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.layout.proofkit_auth_users
+
+
+
+ com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.layoutobject.text.code
+
+
+
+ com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346
+ Session
+ Session
+ com.fmi.layoutobject.text.Session
+
+
+
+ com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE
+ User
+ User
+ com.fmi.layoutobject.text.User
+
+
+
+ com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC
+ This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+ This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+ com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app
+
+
+
+ com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.layoutobject.text.id
+
+
+
+ com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.layoutobject.text.email
+
+
+
+ com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.layoutobject.text.expiresAt
+
+
+
+ com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.layoutobject.text.email_verified
+
+
+
+ com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499
+ Password Reset
+ Password Reset
+ com.fmi.layoutobject.text.Password Reset
+
+
+
+ com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.layoutobject.text.emailVerified
+
+
+
+ com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.layoutobject.text.expires_at
+
+
+
+ com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.layoutobject.text.password_hash
+
+
+
+ com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A
+ Related User
+ Related User
+ com.fmi.layoutobject.text.Related User
+
+
+
+ com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630
+ It's safe to delete this record if the verification has expired
+ It's safe to delete this record if the verification has expired
+ com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired
+
+
+
+ com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E
+ This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+ This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+ com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again.
+
+
+
+ com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233
+ time in milliseconds
+ time in milliseconds
+ com.fmi.layoutobject.text.time in milliseconds
+
+
+
+ com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.layoutobject.text.id_user
+
+
+
+ com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D
+ When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+ When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+ com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty
+
+
+
+ com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.layoutobject.text.username
+
+
+
+ com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15
+ When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+ When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+ com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty
+
+
+
+ com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF
+ Email Verifications
+ Email Verifications
+ com.fmi.layoutobject.text.Email Verifications
+
+
+
+ com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932
+ Learn more at proofkit.dev
+ Learn more at proofkit.dev
+ com.fmi.layoutobject.text.Learn more at proofkit.dev
+
+
+
+ com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD
+ proofkit_auth_sessions
+ proofkit_auth_sessions
+ com.fmi.tableoccurrence.proofkit_auth_sessions
+
+
+
+ com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56
+ proofkit_auth_email_verification
+ proofkit_auth_email_verification
+ com.fmi.tableoccurrence.proofkit_auth_email_verification
+
+
+
+ com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A
+ proofkit_auth_password_reset
+ proofkit_auth_password_reset
+ com.fmi.tableoccurrence.proofkit_auth_password_reset
+
+
+
+ com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA
+ proofkit_auth_users
+ proofkit_auth_users
+ com.fmi.tableoccurrence.proofkit_auth_users
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61
+ code
+ code
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001
+ email_verified
+ email_verified
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1
+ expires_at
+ expires_at
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99
+ expiresAt
+ expiresAt
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3
+ id_user
+ id_user
+ com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B
+ id
+ id
+ com.fmi.tableoccurrence.field.proofkit_auth_users::id
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99
+ email
+ email
+ com.fmi.tableoccurrence.field.proofkit_auth_users::email
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3
+ emailVerified
+ emailVerified
+ com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C
+ password_hash
+ password_hash
+ com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash
+
+
+
+ com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266
+ username
+ username
+ com.fmi.tableoccurrence.field.proofkit_auth_users::username
+
diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/icon.png b/packages/cli-old/template/fm-addon/ProofKitAuth/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..037908f6ad5023cd041e703f8aa755f100b08852
GIT binary patch
literal 38399
zcmbrk1ymi)wk}Et?(P~AJh(dqcMI;a(4Y%beC#@r@Uf&vJ9t~>z1H&d`7
znd>)e8%G{jeyV?Pd4S)4Z!=N>LJlS%9%WJSe~SPuekyY?*p7#h(Z$7u!G)E<*1?RC
znVXxNk%@(oh2=9K@!8SM25jj1*~XCqKtLe-2Se1<(b&Pl4s2m-L-rTb(8$&a%uhvy
zK=yBFVKCUi!stKfHjWI2|J-r<+l2uzn$gh2k&&5!>91O3WPJaU=Mgq>{M-E3AS$Nz
z|1|#9vbOkVBs)U~M^jZdJ5zotWm896CkJEGe-Qq5`ZvfI2UA0^sfhp!6ARmCW@g~v
zWBgwp{T<`KI*7S~rA!3=OX^?k|0yGG1c%Wd$nr7%b)B`vzbyWz@o$s=)8k+M_?O^+^ZEb(IG7m!CkA#-4%Yv~
z*Tk67)Y|l$sSWsV1epIF0TW{$um#xK^uPQsYz_XO{s)M~V{K?-#!uz?*~AoN=wuD1
z5^yB@Oa{2_|6$6%d-E~=H{kyRL+t(=SO2o#`6yeZ5>o?ZLI~q7}_`+
zl2I$0+L)L+m^#q#G5)U-|F2s8OHm+Qfy8C}PkIB5|4a}7AU~A@kp7<%^XwrY;R3H28nvi2G??=j;$t-Y{@^iV{fK_U
zh<}cIOMrNn2qFLHqr6f7cjP{2lf_!%MJ$TYg-BU(o;Fo}=(meKsxxM;?e~JTBP|0bE5qoiX-B;K=)+Vuf&l
zE2S?~8=43l^Sh4|L1-eO@ML}rfRHQ%JQxr*f|Sff1g@Aukh%a!D#ZV1*Hl@m-K@i!
z>^phS&y!&Z=HFV2SGy*)UsBrbk4WF&G$3M$5HSFll*7l7Dvyry4tqW~5k+R*9L6c$
zn{PU(M=xj1YY%HtuZOmrn`46SvCR7ISVh
zNTHWk&l=WV*Q#Cv-YT-LU-2h+i+O_3Bt#)~Loar5=+6{%ujOAGYm&3yhNM9Zf+G>?
z0i$!I?CX!O#BY=9#~)wT-{(`5tH9a!0jA?Az??^!IqrFH;SMo+UVxqJE;V0k+g~3~
zn=@>m|G1y6y+L_i*+~OtPyuHA8lTU(kbj-;R`k9|LR=dyi7d#ex
zCiOid+RgbK$fX6C5E)aW1L%^X|JmzQCdIKNCHfL`b-$+CtNG!Ukn^$PjsJYL;8{l`
zw3pnEp>O|bv+=q0?K$YV-&SWdi{Ao&x6V^JXcP1%yaiiz*X7vha@PI6%ZENk1J0i8
zFMKtlz*g2{lzMndw`V`E;}r9WYIvyAPmj*_X8og;`$_qQ`~QB>NQnC6yS@Zqr-omDba*JGFqQw$?{
z#9Vec^=Ne-H^pp={zU!`eKwP7*lc>Eb}jLEg3XvbeRE!4zb7V>ZD$}aP6DWSUAh9QTI&Uf;`F%&aP_Oegj_o%d2tlbXMnEz&&+E<
z(a4CtPm$eD5fBiCZ<9PWr?7+_vk&K;YEv(QYX@tu?LTam*x{K!1M%h}X@oAhji9Q(
z^0`U+nUpeal0r%QJEsBX^jz((7j@1*tJ|Z#`?1+Cs=*4wXi6#<(Y_%$c_mHIgR=b^
zSD!}5>p>L6k>D&mU_nuNC4YB&-<>ki56&MrMH)B8#$T@icpx5a@
z*7<-@cQosvA7H0my3^2ro>1EVNg+X1iilMF6sBee(}r!-!umFv?)%{
zZO(3CNw3x3&eh&}-cULmkHz4a^ZuGmG@f%O|NQv%Y-GNAXPjTXE&{--#ME*R5osD2
zW;IRjSfP6DdRj%PIc1Z|XE7}ZPWu_!nRR`i>+-gCv(|a0f42HU>PJfKfi5D82ari1
zBZbkG&gTVSYfg7OuVb}=+kl?0$h4{xQS%@jE4KVj&Z+b@Zx#mtNM5bXZWi4#q%f!c
z4hZ3`4WQu-kn-!+RGosdMQHz;jC$*RYjn@mUYAlL=Ji_tRAr8(+t3Cbm+?eTZ}z7_
z?jgWtV7DyVT>i4br6^bAi1Dc+g*kcw@Q&84I(f}G}>*3!&
z;3D9@)u$k&c)yA`n{rfvnB*E|4CWqw>V(Pk_iC2B<<4&3tm0&+Q%LM6zK#U6oCqFv
zEq7k+78pD=pTQD#zn@W|6Axwn0OaSma}fwvzjZ(Bx#({Od2_V0WvG#Vp8d$w{O~O6
zN4L|9x+>Z`>KrhCsJr4C{rxM?_4q{t35<5f;>&r|sNLP!>T%7h${%OTemG_rASm}H
zYqKk9`h)4GHTq*&txd;cVwLpa7ECAT6z!}5uxUi-%3W;Rb%0V*r5^?0jOl;A^Z@({H2Ip1|}xM~LSU>$Z&
zg-$XQodPiZM?d0PShpvK$L6^1aF!QnQqFAt&KP<21Cz6Bb@Gm7naFNxz_XNh{JC>M
ze^e)g`h0027dC1w$(&n-$I(ql&n>4d5FQ*midp|aymuJ;MoV4~4yV`pUK}pBSGXNX
z6&CWJ-yeTM7O5Y*YUEQN`lRF7U}JEF+3zt5b9QoLKa2F0uG19GceSNo*T?H-^)%~D
z7a%0zk2{smlA%~qxe*qs6^li-Bki-@m(m+Uhk+SvofHrF^ir8DtT}UqgHE<_Tb3d4qYoNW7aT(
zzO-XA50X~H+?H;wb_bvOAIpRLr%uu!!YUlwvl}>nKJ1G0Ly!pGadh{V;8N$AZATl)
z+*?Yc-Cl-Js-=<$5ji}Vxh$JIL-#ewt>kie{&FveH!baMDq9Uzwa?~zNXo(0rfjrX
z_`wODp!8kaH@#IGOmXgT@u-HjeSRP}D_+IVL3h3L^FOoJm*9@pMaCK;;6M5?P)g;Q
z?D)KzfHvkCU8%CWf!xe+DYuvx)rUU*2bbx^4iO3d#(P)5g=ozWikJ$&k%5K!k?x3)_R|^L#%G+Mc%9DvJp>?
zgv(O2+>GmkL!2P%9LA9(t_zyx9Jw~9?s4rK9H({43d=%!ztIFw?C%8DGlKh7UG4)u
z*AhpDFL!>;zFa3ufn3yp8>(@}J*d1bddgmx21kA(mas$=v7M5MiKUr&CmE?IlR;qn@`T_e_<=T%E
z{ak~Pi~>2`=eT>ThdCYlSse|VV}|%@OHS`y;*zwjGRPMCOa*UD_1%
z7etHq@2D;h30P39?0DC9HQ~&yAANf9xpBU`6@*lqu9gWc0SbU$J@r5)2=bn(dLQJw
z93BpicvhQ7ja}c!&wOk+)TscqDl;h6POq-=Gql-{c#PtZvOT|0vKKJ-LIxzjS#6-8
z22|~W8t$AK58iHc8-g
zwp_i>&2j!F;+YA6V^f|VhmEW-ZjT=O*e&wv^6q81+7D()-Bm7PYDuLm>>DlXW>@UE
z6>Aucxc5ZT7j1yr`%zt%@TB61#4agn{Z6;E>{CKEn%=z6)_8A9A5d+cVqVc)2^B5o
zDFgY!MbnA?3;~6aeq(LdYoyENjI^`2TTfZk3g@o;jL_m|`JHbJiS;V^=`%bOX
zr&pyFwec)T=;_ful_Z?4do}Crvp)A@HZza6F9ut8zDK_D?HtTm;wP;%l=?6&x1O&*
zYg&u1ZYo+?Q$ShaoCtgUIar6Ot1W)twfOXh?Td8!>cx%UuQlxVF>`8vUMqF6cScqSG>M4D?*=E+Emd
zaG&_j7C8w{jS&k0L2Z*k!m1l>=GwKh3-``m=kdM**?y8$oVJ_NLGgy=)}xxvc1-p)
zw{Dj=(s!JGPk`SkV*qqnTzOL*Z<}8+mE
zD=_nN?E=iNfQQVKpc!?Vh`$pwsP475uQSRY5s3AFWkq9zK0KyCT)T
zFQCc-sFd05S$W~0R8Bj#C
z4UF)(|2$`^xY^Iq_!NLr7_&whvh?k&8o5jSnK#$v>*N=rv;#p2JDH=1jYKlvwY~EB
zj-A|UCJozBqsx_=5GdZf-~+Z|wKw#xqPv}63~QaN_$z%%lECYRc62MwOE+N`Opoga
zALJP9+DvwGw8utX2mO=Uohj>G9R{)AQ6c25c4@v4ET(MzPl@7=OYp=G;bE5~L;mavN*bJnfFC5=n
ze`E)T^z|IfUIWcc1zpHJj4Z~=GIQ*QB9+@`b3VJo2bOz5S3K)gUDP8~5~z$foR;P{nWaetAxN*KSzlH$
zIv0zcDz8o>Y=*2MhD;6MWt8g4rUvxAv4?%eZ|itRit4Y9nb!#0*4(@dxujH;x4bLn
zx8@HVAmw}`T#540kdxad3Ybht`D&BhZsi6C#>^4=kGbX}Dbmnt
zmu?TiIA4~ppH|GBT2~K^%UIkBxHyhZazyhi`o1;w?pEEHw|9}uwdaLbpY@*{0jtnw
zg8<=)Cz=#mlr+9K%3i)&Bzh0fF0lFacKgJtuLZU&NoK82DR-{Z_#f#snIa-8zL;=cfsFLN`-_A?k5vXvZGQjXl;KxcQIOT#
zUG-WDhZVj+Dl=NHu7^6$bNqC7WIR2-r*(DVuk407X2Kh^E!HV_mB!dF7=*;}4NBML
zVLwvUvzL1+Hv=UnuP!PN4l$}7i&UBKQEPF_{<2aTAD>cxmpA!|c?nCmKS(*+lu=?Z
zEikX4Dp_Hg(;ghw5tmVF=R*%{YzrlL51_R-)&fpSmDa;=mneT}TWySK
zd)PmHfuVRn-WIrzqwve_;)W?WZ8dedeaKJpSYyw+LX>Z0IZ{owlDFWnb;tT_TN%BC
zR8a%jRvysAJHwJ5QA8A>7o9I#5`jJi_l4vJi1Y1!AWjo)9BXG-YVsQ!!Tv{nU=iyq
z?X1H;sRnkrH>;(Mt%S{b->5nzki;*YVC3xlRS~m|F!l#ZMhs^79XZ;srRPR;SeeJh
zxpt;1RjS8$l6L%e+{EZOo7zW1X5}8is<_`8HAfaRKey3)xFKL{8X>V2#5%3-snttR
zOmMaN79bjT=Gox+ApqpRjhFAlb^(08x~sPWaI7lIedgIv{XzQ&PC)EBB+r2w-^0fV
zRDF`voVsBu+|?zzXbXMlG2E(^kxJd_QJ&eI%%nW?9cqm=jfNsVdICA#gGqI-4+Z^3
zYr77M#xzHRzc`rZ-0eq#*Y^y^%^Huklz&KGZb$On1|FNaY59fk)8v3w78j$m33-(q
zYtq03RV_zW*PTLQ&1wOo&Az%YhU>De+hNT777q6gNX0{DW1tj$T8*_m>)y6>*7}Hg
zc{lH0G~>G?^kTyW20O*4A&+%JK@ABcKS-yM^``5ZX=y>4B|2$Nv@&kP!&vk&{yR}Oi>JM4M}hKF
zB_-|}&O&2STx%r+0?Z%)otV@)tmDe}~#mo;+q;$_G77!RkkN6r5z<*!?_L
zl5qVKwBpu)&7R)bw{AhXO1gF^=o%pGnEBZNnOpF!3{atRh^%GLZ!sY)DCK*x2QWb=
z4mlf|0`oM3R;x(Qh*E963h7D=f42D9M=ztt7Wuw1o#5#rqzHV4rRrky(X;b4y%4fV
zb276GoeCU{-;q8F{`N`c3kZ+a5AR%-y_U!|ctVOCRQMC_EZV{p=niTLn{BJ1d9?|Z
zIx)-ce}NjF^e9eOOwi6_V&z?bfMMLbq<_=EWqaK2P+^yG_V%7DqgSi^
zIr#hOD_YAD%QsyJYaM5{&uFf{$@bf6QrwS=KTR=6>UgJA)6pd7(xamW(s`Bxdwj3r>g%p4t@k!~
zE7cuM5%oREH6%~o&p8q;H!dETu_5_csWd}Aln3{j6;Cvq#y7h&i5#<^XYo1rXMwnv
zxIS0o<1ZfFx;qCz^M-Tj
zf_88IHk&F6O{v2Yo=#mu)}ZI=(JY)e<(XdJP9~Eo((_!*Kk-yj*7V<9LBT6foUPoY
z%dZr#bB}^q#aMsm;7ODrQ?Cj{ccr4)}+lKRl8YNVzxiMk1x*`p$qUp}i8ACAer
z@^-*0qCP#Bt{F6%Dzc_|3#9D7w+ftnr8qoL#)FEpD2W4W{hT&a52p8?l!YO`9+u
z)urCFC`01FwC@ut{cc(}E$AKq#pWhP6|rjY$ov9d$hsJvx^Tf&IW2u6iU>o!wn#7+
z%N2nn_S-3ds~v$_me-(dqjyEYrjqEd+8p9OuTw^S>gZ}8upG{{rnej7UWo(u3@O!)
zO&mHsta@l?(y+VMsTJ)G+4m}`+3V6)52wXcr8eY;s$oq}4VHJqww?iWw&d^qOHP`sS9%F(jO
zUhBEMZ4;U$G0m%NvJByrWeLS~qe
zlbRe-+NQ6P&sn#%Rk}XxU{OhOJjVRNfoxeVa}u^yvdUFVEYVO9YBL4u#GORK7PdC9
zg#BX1WIsrNft+S
z&VD1wicXxGlher?$L$-ID>aT;G9TrhYihgnY|?W^^=2Bgj1E5SZR@T~XN%3)oo^W(
z-|AC|ts_12Sngc~<;@4kv$#Nv$F-|5Ww6Y`ki}!U7%3yK<^sk%6w`PTdGQTaTqp$>
zHT-!nJ;WOozCsez6hGK7Wm!SL9J%5eHP5ZX<)hSpNKZH}ULZ}k=p8B6
z=*Z1LztiZTl=_-Utc~06?k_#l%n)W9WH2R;FB)E+F-cLA@`=UR3PMM{W4?9w>)~Ni
zhDXLhe={x>HqMMZ*d%eBpsT_VB0i#$>*<4S`?etAZT)%X!oaLG-0nEE?>x*Y)XD;M
zg)h&I!>XB)pH*VeZt@x4=G|Gd^FxpPdx~lndS
z!{U)7W#s8tiz187bV}AVB;eRX5PWql!~z0?Ee785z0u2&;Lo9M3dIbG+}KRR=VNs&
z#)A3K{!hFq!s#*VWc4W!{D4
z{HJU-T@iwt2hSL9M9Ee~5O3V&WfK-h8$U>Cd!%&jf=KC6|#einZ9U3QMWYndAF_xwJS<@5HA)_H?Y>LVf_GZ6U610UfMYg{w0cq9f`woqux4p5s37_aG%T3O{9xSM~F#BA_LOR0v
zzF5d4yZYJfPVpG|*gPYKI%_Grcd92J6bn}mF2=RnM|O^M%D1^vDuxbFn6bQkcR5&g
z1jsQ|^C{mN{3R##wy(rH`)|Z^AX!Ja5mmEjh@u69@{e+{Ar<4>BLgr`+4R2b*FeZgifiU5h
zZw@e$CMqi?6Xt0SxUkOM^V_T95xVQYqAN0M6%=|D4B57PwNEH*=a#w3r6)BE*Dj0yl$c)6+{Sb|
zwRv?du2D3do_kjPU=>>w<%~+4!-K`kd!mJezrHZT6K6Vx&@Di1xDbh~(V#oM)9PrS
zFGkl$wNFYIqlv}*N|9&ahDDFskHOhQ*wNZKEGvIyy|^XYCF(r#_$ML&|2Tx;b&=>u
zbZ&x5xNwupu%+u62Bt1q>4j|psZGbW;h}c5SKd;!GJ!r_GgiKcVzAF(tqk((qww#$
zd4lJ-n@|<#kcuQoIz6eia0Mo4*mMU4Nt2YPkCKXhJVIPnpJRfgX2T&*!P*_I%b%hV
zwxbA2j#*gXsaK;)D`f5K(5dUozzFSqP6VIRcpfv-*QLpCrzsYH}S15`D+`OD`0
zz~W}BmY?GTB)fVvjyzoL7Z#$l#37h0hprf^rsf_~IUVvu9HOc6a@
z>j!N3ySjXXeaHO(6?q=M{CM%9KG;W_l|z#41+9-}^8>%+7xsi}dPhMyO}1P6EbJbB
zz%1v;cN#%w@NL?BTFEe
zc0&nZdQa0%4;>1nF&BLcdv36D_^f+~c0-R=LAu{n5z+8srgb?XIs*T-!~ScjUhxW*
zXk`Nl?Zi8b!7Ej0T0^@1y16s^nv>}$*-#m*D(EpSw^$_HkP)_#7>4k7`?p@+3kSI)
zFu{#u4@Q0}ZYdA4*&Ux*g)NvPH^wS0*qRKTqf0a6Sdjg)19^xpSJTKTu8na@1Kk(Q
z=+1%nG<8Q-)?a@V$qs528}a-p)-6h<{9BX`LDqlNs+-)u+^|VmSRp&39>e;cNwZzr
zs_Bpn^=u#72wISE%*T$44mo1rC@%gmL6L>TziKqp7$s4jA{qd3_q1|$e+*6VWyc=u
zm(M;xLq}0y$IL?CTWEw^rU-=tpAh=gt}C!`5{Ku3rPF`lnT**=U!O)KroHAx64Y~3
z)?1xN$96BFGHIR`EEGe5jf%Yw^Jie{@rLOn*vt7#PS%83J8At9oov>Cj+}yY3FnPvnYP1tFe_AGPut
zPDz!dC`@S~`R4}hc!`h$G>TVHD~Utc3Vc^500+6vAlD8We&j}C|I#y3XTB8RA`iTS
zfNTlwjbv@XM^NYehlK?#)lhOCD6TXIc1C4Xe<;4S_6@s79R_
zIYA|gap1a7frzm$m@$>EMl=yF()8>rCul$g$J7u}z>k#GJ6lh$^~4(}_wh%+YAsU2
zcU=esQm;xzM?l{bkCC!hi0;Z7Koc4uICzAjq`!j@
z9dMECLc=(qyhvqn6=H|X1jJT(|8_>3Kr)s+F{DKu_?^IMmfFlGd&|o`h*W?^dNG2O
zowjj7O+u!Ke$zA_5FBH86S~mTgjJu0urX^DQ5VoJGOdL#3I4Wj?X>NRUz4&wihH~RyzJ#(Z28#_2DSh|;
zj{Qv#PU8f2h5QGTmr;7Enn;h|L*&12G)tC*+`*(rf
zZ!b@#_ZS7ppg^-;>tBZC8RTZXiPk9{C-KY!cYNkxUye0tD^G0cT!m1mFQ_m54Fi=i
zj=301_L3ex<(^fzb(o^}b-$)Gt@*x-ql}jWlcOEW^m!4k(WR+uO60-i1+uF-TtMiF
z_mA{wrYeEx=|bYCmSC}-I*lPy%^_~p%#r)djClhT4{KyGx`U>e(tIGz6`Xdc@|ZQ;
z+#bu)6={r$>P9Ein-2TYPd*kB2Z0kpqr88}qoMax29M&{ICdh_6KRBI_HG_a*2|
z_pp)K-W=L96k_9jz3YhXH7hwY}cAoPd8M@xUCsa
z9@eCk6r8jyRcfwVf1Qy@GkbDgoes+~9MB+X5HKh!^?iS(7ys!df6cer+2rx|V2
z>^rkylej)KOvY&2s(@|7d!qsgqlUWCRIRy96M^B#qv$?luszGl4uDvNek>L9jj{1D~Si%YD&|MAPuFs)+hyf;wQ@It9+0JwLds
zIvzb=4koD{Mb4i4ksNUe*=T1F5v-~swWf(^m)S-}H2(bh6=$VY^$7Tui}XFU1_|`=
z`*OAfUW*jbLR1!!nD@mdfdbf=4G60;!7IcTcrcbC&TWX~eP-+k3caESd5mLA-I-Rj
z;`=J-`&vgf`5}Da)ySLgiQ<57#0C?+3nkJ6A}7887~B}DF8k;_#VcXJcVkc2a8QrJ
za}#&A%ZccZg^~@2pT5-O$pGJw^%)mTxH70A81K{Eafv{BIrQshlQ!n6q?Quv2?9_y&WzTuUevh_)$-cL3||>C1j?hqez?f>^7C
zXA0(bp$)S^|C*%>AcT|Lkok%DRCj!N_B+Yf~
zz?@Omz3gB=J(HN94dLztZUY|Rv0Fz@B#N6;SrNFr#$O!MQHX+*sb9qByoYV!uca^#
zxQLtwTKvS8PFA+aYrL?5PwI1rnr_WXt1RE5?r4rx0x7Ityvt{hW0^Mx?(0nN9+-T0*oC3
zP%&GhVoq3mUuvj*lc-my$dj=ha1_my{ufltD0Xm3%SWMVg)F|fXszC3hmalu_jHw0
zh$f4ClR$<*$v6VPSDlrPeRfiw!OgyzAzv_S6nnh+p$lAqLyNgE>9
z1q-zBe0^f#N1d=hxMrGC@=3)_4Nh~Bo^cZvK{en(S}3CT;!tVY`sy@sM?k6ziPc!q$Vb~f!cTq4BZwTi~vRqAiF_#
ze>}PeBAs$s+#*V+(~Ez|UMD|c4lHljP?o=iIO&zg9CcKRSAx(Erl^mxB=+OfzUZ3~
zA3*^$4wWX5Qo-KOiw<5oF*ll!2|i(PY0()rSkMO2Xw$BUS0o0tC79&Mq%>+`%cdNX
zIP$%9xXc?-oBW%yGkKq?%-zCLuJN9oJvKn~hTN>{;Kyh-X843eMl)tvrm3t?0FgPr
z6}HnbC?j0g`cpwn=vI~}i2I&PP2
zl|wcb9pKj|1i7&Wl`6xD2;&xfe@QB0gHkV*zb5E@aar&_1r4c}q7=7&oFHW-d1J}G
zKf9L3Fv|a=ES)M)_9R?y>ARyHJ{pr^
ziVz6|eMq=W^sw`7pZG%jAUi2%1#vJ|K*SmaOy45S$h<(8Z_|D0mm4MTT9S)XC;50W
zRYs_-Rg4K?>Vld4vai^^TWbczHc>{X3%)xQ&)$tt-saI`CU;c@r&wAIRgMAOwkw3&+1
zOvs@%Owf)7%bx0>;L)DsXNu$kyv3Ww-M8>bivB7HPta=$jp*+#|VqhH(jy!wJATZlT6})*5L2+$&2>J2`
z3jy)=cHFs_@Nv;;?PVoHQ?qL{mAIP(F&65Rq82Pe3)UACNrb?WxARxQGr}`m?s#`h8^9qbF#pCGjPiJs}CR3I|60#zn|E@wU42#xJJLx_^`DtEnnbQ_8VZydq
zTaSSqs=Df9$U}=wPHTE6s)0rE+hrqKObun(#JHf2Ki4drsk0L;M4*Uy+Gg>t8E3OE
zPGUZM8)=2;kq?XUNa-*!#Lx=u48rVyIy8N7NcMz_=ROhy+Yq2p)cLhLqyiE+tCajZ5S+j>o|32&0!@bt8ke_C>8haqA-lmuOj~5Ll;5u*<;W+T!9;le@w>^9fL_^$Yd8(
zha6qk%+lPYHrv(zcJ+6Kkj*twoG>ne*f-q|lScbpo1!2ZAu|cZkB6v0cd`}PD>pKx
z%&6U=$Z+S)MNH0YmS;Nf3PLjHK^WHyzu0zXxnmI&&cwkgoQ!Xe#b{H;rt_;imz+@G
zmdX#TNjTApq+ro;U}Q$Ao%RV9*b<)?Pps`luf5uSy?g>3`O}nT2)!?WN!I~a+K;Z2
zW+x28PE3g54w7jmZO?{zxP^wn%SDGsIHwQktU=o1%|iaH2v0-H%S@=g@SInjLPwjt
zfJk^H#-E9H-w$hPAUiKI7@~nEUH*L#CK#T50v(JJEJ~XyJ0%l|%i#FjqWg1>iJx=$
zsw^kzHp4V*y%lE@{;vk)7zj9Ke_-@|Me;~MPURu~qxP`j)cex6q@XP=YbKdyybw|A
z<5$X1sPkbjSPx8;?T*jiZk1=jSbBV*L<%LU@MzUt^%8-^++i*V)BTbhjZdo@hwyFC
zgmr^i`sxIb|(`yrsnPt~)V6D;&G)$PU`7P*T+j?dE9?OSn6@>?neykE1j}4H&Hxr?
zW}YLpS_hIB%$M}CHbhPM@*Z|=Y;)Wj)8QsmdFn7aQTd}wMQ!Lmfy^~c&
z$42yLQD)nlDm)~R%_iLVkCs141(bB2UB;a6CPLeqJQzfd1=8I`_>P9C)rC;x`#xn0
zR~rRJ05)LglZx5EYk#VUBahPQQaHr#7kMnGypOan3Gn3Dp5#B+*2Jl#g+y*0k*o}9
z%5$w+Nx8Yi>lP2AdNuNPTLnYY@T+$;?+-xuFF*X1A1WPM&2D*p#icep)W7gaSEz^i
zlO>8S{vo>4kYbWcla3VvfY$oq?wwrLIO!39BgT5Bfetq*h!Urfuv!d4>}UD|hz
z4?Rd=?qKP8v;9;77t3Qo{DQKsHj~Vtic2(*K6}lJL?23Jx%U6_eqt%XiaA;+6?+=P
zOp@(sFG6Uz{`@gIpffCKYvt}bCe4*o8iP?M`hqR146}ro61he`$sqkM#spIR-h(@+
zVFIAqMi^5loBQN95V`CKg8S_E((PAhYUl>O=;m8#a}Sk*m18UJl{3W|;;=o2UlM6N
zWjyu>buA);CW9~JEZ36#<^%6BuJdzqALD-At4C=5V!aOUr}^RoQ6J_YxXw`0YX%`%
zKbjva{O6T*3yPubS1R
zMP95w$#7wniUTY}2zMw_v`2`Kdz(MN5mf4|O*fD>FI;k9Pn+BUwG?y4PrJ~WU
zti2zaqVyT(P(l~t&_u+S-27zuL1Ku&KqX-pE><1|?=*8cs-YA4%HG)uH*9shDciNw
z+cLpUd1Irn=>UJG^t>3%FpTNp30-v8T%=Aer1*=jQ+Y5R$(z-X=7qYrVr*~$Izg!*I)3WRL>{$J|gs}oAS;36xOA(qn=um%AT0ZQHvCI)L
z^cs+}EkvB!AsQs+nVE7ZohHe_zWjhR9(WwQ@_iS{%tjP;rWba;FhZR&{{r{HtT4Wo
z`#tCV*i6JH>!|o{e}BeaG-#a>NG#QfclRn=qM*bEG(w`|O)Sr#a)tq_p%;^aOwi%)
z=4XMYvBm?@)&cdt4IVXr4GyS}1Wk*&`&|>5*VoivZ!ROu>j2t%bRn_Hu;6?MsIo$7
zHgK%J+D=JI9=I3+;d@|aFQ4O+IdzS@PZ37{ysHj!O^i*#x%us9C!_KAiYXj#)5qn)Sm-z6Ot8q!>YT1)la@ih-XK>NyZ{zwO
z3W(}5KT79X+D)g&B8WihY$5T-GY7_J0PFq)mimNgZnwQ$`fx0%SZr#oyh<=ADaB_6^#&i$^BMIkFTq&lEQ2xjAQv=@Dc7eue;2QANV+I=|FZEIH5%U5LRQ07s1VGjNX=)-8%r_&jKSVp^Fb
zQsCev=UOXHz{dF&aNjy5wg^yvVI=X3Y;xLb)vg6#Dwe>(0=9)bwB2`K5Dk1qyV6*V
z5s)P|`stWS3RsnKV_IS4LN7qGJr?JVYUVV(t3TujZv0afOKP&(0>6k3~Pa_-2eTsYwXa%`;EisF*7D`Nn0OMd$coSV)}wfBjb)NbqT;^2KYA4G=9-6CZj%9@a~
znhtYV&tN0bl$J1pO(6#2v>*aMN2m9<|qMDmGNz$t?gluafZ
zOC5%-U1MsU4fVmVh^Wfyaj*qCw>Co4*9ehEXTw%O^fvDF6R3Cg2%JG@Y9p)^>}~Xy
zx~g3K7j
z?ti(<=F4Ytg-K5|jKaGM$|`~uWnQz9VyTtPLwFVKgkdKHLOtBu9ZlhT7j%+O?Z}EP
z?5V;b?f%$zG;QUc*fT*W*R^)n8Bcro5t|;4>xK_fOJwrVi_Ef6?<*mNnBjueq0UXt
zHKWNtLyO^FcLtP}xa}(cqULak)ocEY0e(*n4d#p1w@#Tv@nWS~Af3~#kMHJnhFEacbRl3{
zp&CT7bSKCYUCr_hBP$$WpR!w#Iyad18Is6%Wrla|`OQsX65IF{grtmuzMZAk6nLCodIUg}XOwJK1F^(@6CCk-p%_k(!#?`MS~Vr-qil{%4uYs&qm@*-URw{EfJ9ZeiOHcv?P>U;%kPpf={^
z4sKwJ&3)hL&1Z9P?M$cnYa({K({i!)^Siu#F)t-^4a8F3DpWkaZqkpH$JJN{
zR}EQF%HR)-P>6NPEEM~7iRVVp62`+ziY+$0r~2c}l|_C~#6pFecM?BGpYLfIjmJ4%
zqF*KZ{c2i=vwo*E4O}cW1w^O3N*`E0^mBIKBn@Mn;YLj+>riA9c0
zgz20I#oT;vO>R^}v%=pD{@E`iiy%mD0|W0aT|*j!+7M+QhhOq!Sq3RwWhE}vud}ff
z{$n^syfk^ra@3mbi0>x(-N70d1_?4cd;9>o>H*|3%X`_~+F;Z{M+PyRnTXP0}Q7
z)Fh2OC&JM1*=Im~8i}C`a+jN3xYVKHl%1g*7
z-x~b=<{10$LVsv4>56!?V8Wpm!fO4c8GZpM`z|GasUbcpw%VzqJdM31
zWYC7D*fr50#P|kPBZFFHwEZU-LCtzReU{OP#Apr4O}0
zZLY~als7VXiu!y1nV6bj<%QO5BLoiSecm1(e+Y-rGD6~gIpER9C?*?(sz{!IeMBlamPdy**-As{D}px?boTP*O*?n`L(G?N{lUc#f<
zk6`uklKx+=x@Lc+A!H$-Hv3{ORMF=iAtDBCfq-nCR-{?Tw6iOXZHWZ8JzX#5aNn8~kaglHWK|kRISP
zpkk`1U7svN+X_I=)KxdwD``t|a%TNt&uFz~65^IcgVr4Ke?3VMQ)&~$$8?}`@>$GV
zQI30;{CkfliAK-ZT(bV8-eG}^rLvAl7k$(3<8kcKWpZvDoIN_W@IqkvAx#MqZ;BY{
z7wZ8=Y&LlQjlZ9pikzn&C_{Il{|rlrqdtgY_XZz)No)DwA3+DBQ6CMr9NT?Z)72rl
zN2S#6r(?~JHzI0_`e){TtA2I)J`*2vUPL;HQQ6Rh(v
zT_4J!3%?jjnTk1Y=CxBc`cVkpU!o9QXaTvSacpR(!O&jQXbl{<1{|Ul@nF$z(uXhp
z-mI(bifUF#3~D&HRGdRCoboWq5|vgrrl!8_f3Y8Ff56tX2oRser)24fiu&grI=C4QJM^uf`?*Fdv~ORyD+HG~CGwNNg3L4p|e
z9g>gfWo%J(zWcP0*=m6O+sq|Z@+j*aPqj+TY1(RsORZ)wW67B?S~SsCb~mx(1lJQmpu6&`Mv#J?c7i-L
zf-^ep=0+adZ}e=zPhkk3dY2g#07R$!w;7+LsLqm%75w2IbR;AwagztfeMIPjNk6@i
zG<7}OZ=N5=pmLL>@Fy?597YD_{TT6JGVSVkh-kK813Ea2Hgu8sSMr-{XtLZL{^jB~{plhCHKOTez?p!@IMYbL@
zqBw?(eW{Q+Fodg=Cyft5Gbotz6=x4*16X!~)}3j<{vaVd!Tg
zVz^S6OXiO4kbCHiAIW$bmcjghQwH@UK#-Ou>ZcLX?Mjqrt!&C7$0OK2uZjRzyf7I0
zEdn99uag~jrCwKSVFqY25#yL
zAjv{vQW2-+NGKFhsuH_Ct8|et_*8XR2Fa?2Qmcs@l9K~Y^K#ererzV=e08di{Ji83
z9mZd)9>^hCNv*iu6!&7+Mq#O4@F7HCK*g8q$Ds#_@EmacnE?2?;ZCzI+-oy3H-~iz
z-H~I6#4I^Y&ld5u+z#42alxS5igd@F?P*BRJL$?=j-2C=dizPNRs^wdYdJr0vsU#uuGcwM8tyR?HE!o8xWijE%^OixVp
zs)_IX$UZ!5ANm_Ir+mG8uKrHd;*|V881Sb|{8C9>YiLd}zDO;mv-{eS6UmOdQO+Oi
z#6T|k@yk=~k&I=Izinr5a)tmzx23JJvu{PQ$ndYGBRi!9xHaM}b$b}h$ORFtjwspJ
zKWI6+{MMegA(m__L~IwroR0p06PxP;ljZuDA|Chov{Fmu%*|A?2ePN&4pB1S^`K?N
z`d&uP&*v!$bQdo)kqM!n9Z)lWyY81>C6__4B_J&4OO*_z=IUBuP_{<;<$LE?k4szZ
zzPvC-7AX*-{;F#qu8ig;@1kY)aO+>l(M_9jJ};K&Q%2An0ocKa5yAVT7M(^0B63n?
z;IfrWCgm#~$R=lZmzr_>Mntvt*Vql1b5h`f<--N?w46!+Jho2;f<2)XUE&Lmh5Vz*8(Y^G%5cz%AQM3
z5;w|Hx-yI=ThGHO8TiGaXr#@}rBH+gTa%<)>chF*jVd5A4Yb9u+8CZqI87Ws=or|E
zL=6oL%|)II0OaW0wz`B51P<0F(h0Q~BvJU|5{*
z{%!w?IQ2fU`EzuRZQ^Vpg8e`|<_Nr;RGw*?g+@C+qT-Ud8WFXTE63w0vABRrY_1u#
z@^$&TYwFXriE4k5RneJlGx5AMWL{*{n#hkwC-VuU6vO$fR}}ktpEV=jf{9FZCCNT+
z7jIITumJK!&5=4h=XP+wXU&m#49wfewcBQ&`R~DLKQ9ET4ac|mpMOpgIz1oaW$nVY
z0DNUMvOWKX9S@f&dJ)!^3oTGIlZkqTv@6pr)T}}vqzU(3KrL8j8?aYyfPV@DD|WQv
z;ed@xGhtS<;vP72YN(!k9%bc$_P(q=hURnOBCy>^YgO{iEW4*ReB7=?
zbOEw`CZrfo50%Mh#VovbZ%|d88l3jFQEE#LxwUdLxB8nrl-o0t
zQT1g#BNif>@3VSS1QHIXzc>1`=B_S;KBKjb-_?x#-gF5VMceDcppra|3deU#Gf_
z@9KC;-ZaNmgmc&->##Z_Biqm~Ex)qp@Avis5WwaQSWv1WSPKq>+nBDMW;ysGj_cZ9
zexicr@Wi@hEP+92BTdM&-(1otwk#KL5#l^NLMac_q!%Th#J?fMh3TpF-RvUVmX+`)
zdC8h8Ud#4gau~VlUp2vW+?i*`B+X`5wuSSJ`tDnl^!9#ULgu{#+*|ZtBBc;rGUh#d
zbrU2t_V%3P07s*OrYja_ek6|*4}*Y)p|ACC!$fvQ813X_AtbEu4-|WdVh%xvldFCB
zr4v)=P7>%EWfIxl^GC@RA1GzprC?~CggASVHROAd$!FILjZmyr^_RZ|jRMemRaG&Z
zDq(inQE1>ys3+qCOk^cUl9=5j8@RXFPdymL2?K;XzX}cZHatY(Wq{(Dh@t85f$IxE
zQGu-3+*3y|r_l2JimB;nFo1bD<#A{Pzvj=`nxz&mVkTBn8BAJ93S!To@NJ|LZk0^P
zXG%Vg!f)!@5w?F+#>E7iwrjGIzDWSSqzQlrNHtiW^;x=hH4T(zNmFPNP_{-Ti^@u;
zM)sJc{E5Iz({AYQ9SLANr`*PT?uKpyfUD+*p*(hIYRr}LoNTKO
z!`6*Ku8OTl=<{+++s6-J#qL1igQ86g)`?tJyM)Tr2&$&ew|hI#FyJ|9zWbDy!k=q$W@9m^LuDaWA(IZZdaQeEMLB>
z(=J6