From 52a588ecb55c552605f453e3ca139d38f67e23a3 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:29:29 -0500 Subject: [PATCH 1/3] remove CLI-old --- 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 | 129 ----- packages/cli-old/proofkit-cli-1.1.8.tgz | Bin 852080 -> 0 bytes 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/fmMcp.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 -- .../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 15086 -> 0 bytes .../nextjs-mantine/public/proofkit.png | Bin 52140 -> 0 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 15086 -> 0 bytes .../nextjs-shadcn/public/proofkit.png | Bin 52140 -> 0 bytes .../nextjs-shadcn/src/app/(main)/layout.tsx | 6 - .../nextjs-shadcn/src/app/(main)/page.tsx | 137 ----- .../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 - .../cli-old/tests/browser-apps.smoke.test.ts | 99 ---- 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 - 378 files changed, 22367 deletions(-) delete mode 100644 packages/cli-old/.yarnrc.yml delete mode 100644 packages/cli-old/CHANGELOG.md delete mode 100644 packages/cli-old/README.md delete mode 100644 packages/cli-old/index.d.ts delete mode 100644 packages/cli-old/package.json delete mode 100644 packages/cli-old/proofkit-cli-1.1.8.tgz delete mode 100644 packages/cli-old/src/cli/add/auth.ts delete mode 100644 packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts delete mode 100644 packages/cli-old/src/cli/add/data-source/filemaker.ts delete mode 100644 packages/cli-old/src/cli/add/data-source/index.ts delete mode 100644 packages/cli-old/src/cli/add/fmschema.ts delete mode 100644 packages/cli-old/src/cli/add/index.ts delete mode 100644 packages/cli-old/src/cli/add/page/index.ts delete mode 100644 packages/cli-old/src/cli/add/page/post-install/table-infinite.ts delete mode 100644 packages/cli-old/src/cli/add/page/post-install/table.ts delete mode 100644 packages/cli-old/src/cli/add/page/templates.ts delete mode 100644 packages/cli-old/src/cli/add/page/types.ts delete mode 100644 packages/cli-old/src/cli/add/registry/getOptions.ts delete mode 100644 packages/cli-old/src/cli/add/registry/http.ts delete mode 100644 packages/cli-old/src/cli/add/registry/install.ts delete mode 100644 packages/cli-old/src/cli/add/registry/listItems.ts delete mode 100644 packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts delete mode 100644 packages/cli-old/src/cli/add/registry/postInstall/index.ts delete mode 100644 packages/cli-old/src/cli/add/registry/postInstall/package-script.ts delete mode 100644 packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts delete mode 100644 packages/cli-old/src/cli/add/registry/preflight.ts delete mode 100644 packages/cli-old/src/cli/deploy/index.ts delete mode 100644 packages/cli-old/src/cli/fmdapi.ts delete mode 100644 packages/cli-old/src/cli/init.ts delete mode 100644 packages/cli-old/src/cli/menu.ts delete mode 100644 packages/cli-old/src/cli/ottofms.ts delete mode 100644 packages/cli-old/src/cli/prompts.ts delete mode 100644 packages/cli-old/src/cli/react-email.ts delete mode 100644 packages/cli-old/src/cli/remove/data-source.ts delete mode 100644 packages/cli-old/src/cli/remove/index.ts delete mode 100644 packages/cli-old/src/cli/remove/page.ts delete mode 100644 packages/cli-old/src/cli/remove/schema.ts delete mode 100644 packages/cli-old/src/cli/tanstack-query.ts delete mode 100644 packages/cli-old/src/cli/typegen/index.ts delete mode 100644 packages/cli-old/src/cli/update/index.ts delete mode 100644 packages/cli-old/src/cli/update/makeUpgradeCommand.ts delete mode 100644 packages/cli-old/src/cli/utils.ts delete mode 100644 packages/cli-old/src/consts.ts delete mode 100644 packages/cli-old/src/generators/auth.ts delete mode 100644 packages/cli-old/src/generators/fmdapi.ts delete mode 100644 packages/cli-old/src/generators/route.ts delete mode 100644 packages/cli-old/src/generators/tanstack-query.ts delete mode 100644 packages/cli-old/src/globalOptions.ts delete mode 100644 packages/cli-old/src/globals.d.ts delete mode 100644 packages/cli-old/src/helpers/createProject.ts delete mode 100644 packages/cli-old/src/helpers/fmMcp.ts delete mode 100644 packages/cli-old/src/helpers/git.ts delete mode 100644 packages/cli-old/src/helpers/installDependencies.ts delete mode 100644 packages/cli-old/src/helpers/installPackages.ts delete mode 100644 packages/cli-old/src/helpers/logNextSteps.ts delete mode 100644 packages/cli-old/src/helpers/replaceText.ts delete mode 100644 packages/cli-old/src/helpers/scaffoldProject.ts delete mode 100644 packages/cli-old/src/helpers/selectBoilerplate.ts delete mode 100644 packages/cli-old/src/helpers/setImportAlias.ts delete mode 100644 packages/cli-old/src/helpers/shadcn-cli.ts delete mode 100644 packages/cli-old/src/helpers/stealth-init.ts delete mode 100644 packages/cli-old/src/helpers/version-fetcher.ts delete mode 100644 packages/cli-old/src/index.ts delete mode 100644 packages/cli-old/src/installers/auth-shared.ts delete mode 100644 packages/cli-old/src/installers/better-auth.ts delete mode 100644 packages/cli-old/src/installers/clerk.ts delete mode 100644 packages/cli-old/src/installers/dependencyVersionMap.ts delete mode 100644 packages/cli-old/src/installers/envVars.ts delete mode 100644 packages/cli-old/src/installers/index.ts delete mode 100644 packages/cli-old/src/installers/install-fm-addon.ts delete mode 100644 packages/cli-old/src/installers/nextAuth.ts delete mode 100644 packages/cli-old/src/installers/proofkit-auth.ts delete mode 100644 packages/cli-old/src/installers/proofkit-webviewer.ts delete mode 100644 packages/cli-old/src/installers/react-email.ts delete mode 100644 packages/cli-old/src/state.ts delete mode 100644 packages/cli-old/src/upgrades/cursorRules.ts delete mode 100644 packages/cli-old/src/upgrades/index.ts delete mode 100644 packages/cli-old/src/upgrades/shadcn.ts delete mode 100644 packages/cli-old/src/utils/addPackageDependency.ts delete mode 100644 packages/cli-old/src/utils/addToEnvs.ts delete mode 100644 packages/cli-old/src/utils/formatting.ts delete mode 100644 packages/cli-old/src/utils/getProofKitVersion.ts delete mode 100644 packages/cli-old/src/utils/getUserPkgManager.ts delete mode 100644 packages/cli-old/src/utils/isTTYError.ts delete mode 100644 packages/cli-old/src/utils/logger.ts delete mode 100644 packages/cli-old/src/utils/parseNameAndPath.ts delete mode 100644 packages/cli-old/src/utils/parseSettings.ts delete mode 100644 packages/cli-old/src/utils/proofkitReleaseChannel.ts delete mode 100644 packages/cli-old/src/utils/removeTrailingSlash.ts delete mode 100644 packages/cli-old/src/utils/renderTitle.ts delete mode 100644 packages/cli-old/src/utils/renderVersionWarning.ts delete mode 100644 packages/cli-old/src/utils/ts-morph.ts delete mode 100644 packages/cli-old/src/utils/validateAppName.ts delete mode 100644 packages/cli-old/src/utils/validateImportAlias.ts delete mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc delete mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc delete mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc delete mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc delete mode 100644 packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc delete mode 100644 packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc delete mode 100644 packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc delete mode 100644 packages/cli-old/template/extras/_cursor/rules/ui-components.mdc delete mode 100644 packages/cli-old/template/extras/config/drizzle-config-mysql.ts delete mode 100644 packages/cli-old/template/extras/config/drizzle-config-postgres.ts delete mode 100644 packages/cli-old/template/extras/config/drizzle-config-sqlite.ts delete mode 100644 packages/cli-old/template/extras/config/fmschema.config.mjs delete mode 100644 packages/cli-old/template/extras/config/get-query-client.ts delete mode 100644 packages/cli-old/template/extras/config/postcss.config.cjs delete mode 100644 packages/cli-old/template/extras/config/query-provider-vite.tsx delete mode 100644 packages/cli-old/template/extras/config/query-provider.tsx delete mode 100644 packages/cli-old/template/extras/emailProviders/none/email.tsx delete mode 100644 packages/cli-old/template/extras/emailProviders/plunk/email.tsx delete mode 100644 packages/cli-old/template/extras/emailProviders/plunk/service.ts delete mode 100644 packages/cli-old/template/extras/emailProviders/resend/email.tsx delete mode 100644 packages/cli-old/template/extras/emailProviders/resend/service.ts delete mode 100644 packages/cli-old/template/extras/emailTemplates/auth-code.tsx delete mode 100644 packages/cli-old/template/extras/emailTemplates/generic.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/middleware.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts delete mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts delete mode 100644 packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma delete mode 100644 packages/cli-old/template/extras/prisma/schema/base.prisma delete mode 100644 packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma delete mode 100644 packages/cli-old/template/extras/prisma/schema/with-auth.prisma delete mode 100644 packages/cli-old/template/extras/src/app/_components/post-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/app/_components/post.tsx delete mode 100644 packages/cli-old/template/extras/src/app/api/auth/[...nextauth]/route.ts delete mode 100644 packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts delete mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx delete mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx delete mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx delete mode 100644 packages/cli-old/template/extras/src/app/layout/base.tsx delete mode 100644 packages/cli-old/template/extras/src/app/layout/main-shell.tsx delete mode 100644 packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/app/layout/with-trpc.tsx delete mode 100644 packages/cli-old/template/extras/src/app/layout/with-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/app/next-auth/layout.tsx delete mode 100644 packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx delete mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/action.ts delete mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx delete mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts delete mode 100644 packages/cli-old/template/extras/src/app/page/base.tsx delete mode 100644 packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx delete mode 100644 packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/app/page/with-trpc.tsx delete mode 100644 packages/cli-old/template/extras/src/app/page/with-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx delete mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx delete mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx delete mode 100644 packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx delete mode 100644 packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx delete mode 100644 packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx delete mode 100644 packages/cli-old/template/extras/src/env/with-auth.ts delete mode 100644 packages/cli-old/template/extras/src/env/with-clerk.ts delete mode 100644 packages/cli-old/template/extras/src/index.module.css delete mode 100644 packages/cli-old/template/extras/src/middleware/clerk.ts delete mode 100644 packages/cli-old/template/extras/src/middleware/next-auth.ts delete mode 100644 packages/cli-old/template/extras/src/pages/_app/base.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/_app/with-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts delete mode 100644 packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts delete mode 100644 packages/cli-old/template/extras/src/pages/index/base.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/index/with-trpc.tsx delete mode 100644 packages/cli-old/template/extras/src/pages/index/with-tw.tsx delete mode 100644 packages/cli-old/template/extras/src/server/api/root.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/base.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/base.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts delete mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts delete mode 100644 packages/cli-old/template/extras/src/server/data/users.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/db-prisma.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts delete mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts delete mode 100644 packages/cli-old/template/extras/src/server/next-auth/base.ts delete mode 100644 packages/cli-old/template/extras/src/server/next-auth/password.ts delete mode 100644 packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts delete mode 100644 packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts delete mode 100644 packages/cli-old/template/extras/src/trpc/query-client.ts delete mode 100644 packages/cli-old/template/extras/src/trpc/react.tsx delete mode 100644 packages/cli-old/template/extras/src/trpc/server.ts delete mode 100644 packages/cli-old/template/extras/src/utils/api.ts delete mode 100755 packages/cli-old/template/extras/start-database/mysql.sh delete mode 100755 packages/cli-old/template/extras/start-database/postgres.sh delete mode 100644 packages/cli-old/template/nextjs-mantine/README.md delete mode 100644 packages/cli-old/template/nextjs-mantine/_gitignore delete mode 100644 packages/cli-old/template/nextjs-mantine/components.json delete mode 100644 packages/cli-old/template/nextjs-mantine/next.config.ts delete mode 100644 packages/cli-old/template/nextjs-mantine/package.json delete mode 100644 packages/cli-old/template/nextjs-mantine/postcss.config.cjs delete mode 100644 packages/cli-old/template/nextjs-mantine/proofkit.json delete mode 100644 packages/cli-old/template/nextjs-mantine/public/favicon.ico delete mode 100644 packages/cli-old/template/nextjs-mantine/public/proofkit.png delete mode 100644 packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/app/layout.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx delete mode 100644 packages/cli-old/template/nextjs-mantine/src/config/env.ts delete mode 100644 packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css delete mode 100644 packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts delete mode 100644 packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts delete mode 100644 packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts delete mode 100644 packages/cli-old/template/nextjs-mantine/src/utils/styles.ts delete mode 100644 packages/cli-old/template/nextjs-mantine/tsconfig.json delete mode 100644 packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md delete mode 100644 packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc delete mode 100644 packages/cli-old/template/nextjs-shadcn/.vscode/settings.json delete mode 100644 packages/cli-old/template/nextjs-shadcn/README.md delete mode 100644 packages/cli-old/template/nextjs-shadcn/_gitignore delete mode 100644 packages/cli-old/template/nextjs-shadcn/biome.json delete mode 100644 packages/cli-old/template/nextjs-shadcn/components.json delete mode 100644 packages/cli-old/template/nextjs-shadcn/next.config.ts delete mode 100644 packages/cli-old/template/nextjs-shadcn/package.json delete mode 100644 packages/cli-old/template/nextjs-shadcn/postcss.config.mjs delete mode 100644 packages/cli-old/template/nextjs-shadcn/proofkit.json delete mode 100644 packages/cli-old/template/nextjs-shadcn/public/favicon.ico delete mode 100644 packages/cli-old/template/nextjs-shadcn/public/proofkit.png delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/globals.css delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/layout.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/navigation.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppLogo.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/config.ts delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/mode-toggle.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/providers.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/theme-provider.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/button.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/sonner.tsx delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/lib/env.ts delete mode 100644 packages/cli-old/template/nextjs-shadcn/src/lib/utils.ts delete mode 100644 packages/cli-old/template/nextjs-shadcn/tsconfig.json delete mode 100644 packages/cli-old/template/pages/nextjs/blank/page.tsx delete mode 100644 packages/cli-old/template/pages/nextjs/table-edit/actions.ts delete mode 100644 packages/cli-old/template/pages/nextjs/table-edit/page.tsx delete mode 100644 packages/cli-old/template/pages/nextjs/table-edit/schema.ts delete mode 100644 packages/cli-old/template/pages/nextjs/table-edit/table.tsx delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/actions.ts delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/page.tsx delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/query.ts delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/schema.ts delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/table.tsx delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/actions.ts delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/page.tsx delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/query.ts delete mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/table.tsx delete mode 100644 packages/cli-old/template/pages/nextjs/table/page.tsx delete mode 100644 packages/cli-old/template/pages/nextjs/table/table.tsx delete mode 100644 packages/cli-old/template/pages/vite-wv/blank/index.tsx delete mode 100644 packages/cli-old/template/pages/vite-wv/table-edit/index.tsx delete mode 100644 packages/cli-old/template/pages/vite-wv/table/index.tsx delete mode 100644 packages/cli-old/template/vite-wv/.claude/launch.json delete mode 100644 packages/cli-old/template/vite-wv/.vscode/settings.json delete mode 100644 packages/cli-old/template/vite-wv/AGENTS.md delete mode 100644 packages/cli-old/template/vite-wv/CLAUDE.md delete mode 100644 packages/cli-old/template/vite-wv/_gitignore delete mode 100644 packages/cli-old/template/vite-wv/components.json delete mode 100644 packages/cli-old/template/vite-wv/index.html delete mode 100644 packages/cli-old/template/vite-wv/package.json delete mode 100644 packages/cli-old/template/vite-wv/proofkit-typegen.config.jsonc delete mode 100644 packages/cli-old/template/vite-wv/proofkit.json delete mode 100644 packages/cli-old/template/vite-wv/scripts/filemaker.js delete mode 100644 packages/cli-old/template/vite-wv/scripts/launch-fm.js delete mode 100644 packages/cli-old/template/vite-wv/scripts/upload.js delete mode 100644 packages/cli-old/template/vite-wv/src/App.tsx delete mode 100644 packages/cli-old/template/vite-wv/src/index.css delete mode 100644 packages/cli-old/template/vite-wv/src/lib/utils.ts delete mode 100644 packages/cli-old/template/vite-wv/src/main.tsx delete mode 100644 packages/cli-old/template/vite-wv/src/router.tsx delete mode 100644 packages/cli-old/template/vite-wv/src/routes/query-demo.tsx delete mode 100644 packages/cli-old/template/vite-wv/tsconfig.json delete mode 100644 packages/cli-old/template/vite-wv/vite.config.ts delete mode 100644 packages/cli-old/tests/browser-apps.smoke.test.ts delete mode 100644 packages/cli-old/tests/cli.test.ts delete mode 100644 packages/cli-old/tests/init-non-interactive-failures.test.ts delete mode 100644 packages/cli-old/tests/init-post-init-generation-errors.test.ts delete mode 100644 packages/cli-old/tests/init-run-init-regression.test.ts delete mode 100644 packages/cli-old/tests/init-scaffold-contract.test.ts delete mode 100644 packages/cli-old/tests/setup.ts delete mode 100644 packages/cli-old/tests/test-utils.ts delete mode 100644 packages/cli-old/tests/webviewer-apps.test.ts delete mode 100644 packages/cli-old/tsconfig.json delete mode 100644 packages/cli-old/tsdown.config.ts delete mode 100644 packages/cli-old/vitest.config.ts delete mode 100644 packages/cli-old/vitest.smoke.config.ts diff --git a/packages/cli-old/.yarnrc.yml b/packages/cli-old/.yarnrc.yml deleted file mode 100644 index c2e3ce63..00000000 --- a/packages/cli-old/.yarnrc.yml +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 535c5823..00000000 --- a/packages/cli-old/CHANGELOG.md +++ /dev/null @@ -1,285 +0,0 @@ -# @proofgeist/kit - -## 2.0.0-beta.22 - -### Minor Changes - -- 5544f68: - cli: Revamp the Web Viewer 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 Web Viewer 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 deleted file mode 100644 index f8e1efec..00000000 --- a/packages/cli-old/README.md +++ /dev/null @@ -1,19 +0,0 @@ -

- - Logo for ProofKit - -

- -

- 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.proof.sh](https://proofkit.proof.sh) diff --git a/packages/cli-old/index.d.ts b/packages/cli-old/index.d.ts deleted file mode 100644 index 61865039..00000000 --- a/packages/cli-old/index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 468a8075..00000000 --- a/packages/cli-old/package.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "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/proofsh/proofkit.git", - "directory": "packages/cli-old" - }, - "keywords": [ - "proofkit", - "filemaker", - "ottomatic", - "proofgeist", - "proofsh", - "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": "^22.0.0 || ^24.0.0 || ^26.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-old/proofkit-cli-1.1.8.tgz b/packages/cli-old/proofkit-cli-1.1.8.tgz deleted file mode 100644 index 45e19d0476a3d9596764a79a55a888bdf666d2e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 852080 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

({s>PF>UPbNJDHk5xggX_58=@pZ86 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%0A+ZgDiE3DttUtuaZb)uuAbG>bJ@6*r<(Elux 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%0A1b@!crTe-Tr{WDpl17yfa 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�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 z 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 z zIL%dC{MwM1ALp}Lr>)fsvV3F3ExNVhcGNkittg;%uLoV9^e0c-zUR-z2TuKNW3@5P z4p?Z+H~!i9r{*%7wuQ~@DEPO->CC_Z3^TC4YEy?Oz!f6dqwYFgNG4vu=QmAwm&i32 z>XJ!)GZO1HCl`#ws`4}`xEgxuU&vG2J+}Ai^nks3bLH6~XL1Ij6!$$7Hb*wojx+v7 zi0`pUY365BX@6NqZLDq;M<{0Zevd#jA}QcH?Cnh2DmFVY(OjgIKRIH6M4&gNy5;;% zUHZPy>3R!zla+a_9jy4A-NrJp$XbsT?C0KmYJ60+rhT8ipYVKaBv;deL+qPFG?Gcn zHKrP7eL%S^WXejWisA;vV_{!B)^E!%tIj_<4l|6+YJP@~#?-_@@sfOYX#$Fxz)MQR z)Gmayctoee3feFZdz_zm%@r|URU@l{v?qG`nnh_7SsgrwO~-RN_cm46AIMVld-@z= z*@>%G;j$a_YGv%OuQqo0NcLh0o*05c2Ab33Vx`S*8K0`utj+f1XH!2jwRiUk`y0I{ zV18fM!+V!b;O?&6hjjwaHNMkrc4;}@2%|q>j)>t}8~Sa#ihsGEJRR!?^Zz7&B5C!jzH)!#uU_&KH_Cvb!kaV33m6H9f>Ral{>=2GP_)P3&3x|s{A z(Ya30kiYP|@E1(8AjQgioP`KJOV+`E9Ck{nX)-B(E2SgC8`$^m_waCkq!!&>&w};4 zN75|FgnQ*3l)(jCNIAz=3##GV)gW$(I=Y_fEDV+CUXv1CDpq;|ChK#0Y%;hfmMW3G_(rWuenEV zQ=*%0jQ<7-bU*(7o*zXRD$QN~<~Amqz57zV>FV#U`%UlfZmvo8W4FCF?6B*z{>1tO zAgfm6wl1rh*v-`>2}jyO9+gDny{Bkg`F(Xq%O~=V(y?*hL$Pgaes)v6k=89+;wv*v z@(f|mW?h-+O?Te=oCdAH&B$spVAtN%jpFmPYyx;i^k$e&A}+Wo1bC|67Mixy?gQAmo^k36buO8{V@Y+W7^oNEKTFgVv!7FW#io>{ z+ghs@E2rQ^HX+01q}#hH_jk!FxN}8m zr}fF*-;y=<8hh{-yZe;q)mPkpV*il#>q9C*j&${7@DRfTEBPN^^Qc>(x>VYIo|?1G^)xa-h9V*4%9wKX5Or zQUSg?iQ$cQ%p9*d(J4Od@I&oMo8H_~5yU7y5ru5bT)+xPIJzWsKMlffq?r=Otxlm2sZ zeGUD`@k?&{j)%lO_lbPESLsrx{Uz$mKb4&tyjKTb)!n}HNA8uI`>vmEuMB#MFLBr0 zbyvKkNYj+wj0YYH{c6t@3gJm)M4AEr4m*CQl0smAq}ki!@wjPUca0e{O~&;=vWm_O z=0qweKDC!z^`)j_HBWu#R?!Wftgr58DHd8K&?dJ9nzQ5+2b4t8AzX)C{gW1HBwF)m zpz7pRzW>kEoUFl1LQtL>?u<_!lq>2_KrVABxHZX$3iVEU?AMC5f88tSasMIsmi2>M z$7s&8Z_hrm8fe9OXPR~vgE955U@vF=De!1l*K3pX67)j@aK0u8deF(+Lg|te+*2Zs zg{x?Eh(|vU9$iU~!4VI^R2eFD5|!}1L%qhP#8e%=nD>f#2(>0!x^UlJPM<0D+GIUn ztPBtD=u@FB_>;@I9K-^84i238Y?t+lV7@sZL$1_-T+J9O)D0Qo&r32*M|b+NW+uL_ z^Y=P;e?~>gx&%upI{Q=-bM|qn`AHSrBbR=vywfK@|+AoeiIQZr!P|g;aR6q4#+<7F?x{JnJ5@j-x=8c%KL<>eSP6auS=M7>JAcI?tae zGluNeR6Et`pn>Wv8Fy$^dsnJ(DX~^EFnV`;cyLcQ<Ur?Hgj zLho=TVX$&-1xo)D`XhH2SCt|Pg z+s3I4usX4(ZDiNP=Dm9KPh}lzRc!+7b_gC-cJgE6zce*}!|Cu!T(RvOENJ`-c5x4_ z<72Gj`^H>jx-lgyS@9e$V=|lh+qNQ);eGhTTAq|eLiP5#L>rFg{}i0m2tRIJ;)!}o z?#ugl4YqlykNxl%8~tll0Pm-y`!s9)Iz6CGG0xP9=5h`ld|rj0Gk#Ep_i&XxRs1aG zJPz`J&%;(Os5_Zfn0Y$G5!(~O94Xe9V*hyR^(pZA=KG&Yjcb^n_vYswIu8-6P!fF^ zJI$slgo>jgP-2^{kdxB|>VkT$`FX}UD%2^SN>?Z+`fSIh6)~Hu3Co^1JU;&(bpCoZ zi=Qe2jp%1c)RAuIt&)CL0Qy&e5GQ!`rWlECM?tKVn%*!19Y&x}9f6iamYL@Ag{oVm zV1X$fC704w$xg0#tud0@Gg^MN2(-N~hLPtm^8E3h&%^!~uxkv9a5#0I)IVwvjhISN ztXQDXM5U<1A-U@LqG$4ao~BqQXju8we`>DRkm*E|BaDgC#E08#UPse_QL@)V?DbgqGucR~aqIdT{ zhdOvk#UYo1L&Dr8(4haBhnza!zQhz3EFQ zJe79pYdwE8XD=nE&opJsPGh1uk+Ik$rl6BhkMdOm7^FSoJqy>{dMq-|m(6-krFh!z zwJ<%Bsxt{X(yZ7}aiVoU4bR_mv0aT@?GcqYSWKVHq-KRoN!=9Gj0!>+1E?i{S_E=DP%nts%NJ@A;L>Ka}|6}e})w=h=2#}{^!sP^>$QI5`ph+Af)&M@`CCZ)i*`I_Uge zWfDGJJ=b)SN~wigKVL4z(?F_8Hrvyb9}4yO23(^t5q+JFVLf+P&wWfi*OSU&%&w~N z3Ar%Kf}c9-=_}X+G5)PkkNvM!&plv|w!Z0TSiv1uaDPe#H(T?qw91wX zzHSw*fS*`Ln2%0HzaM)pMsAx`>l1&pXH>`ZuLtB(5wT=RtlI>)bci5h@kCra_|T6H z{a8OEckWt7?jMam8vok(SJ=4!`9Mal?Zi}bw`wT>+q`%m?9#eZJg&_Yo^oQg67>3y zN3elSO(>U&mqqC{@v9G#l4^EYrfPSD$E~Fn8+E?>@<|=BYjzrIjW><|;@SLqeBq&Q z9{T2=%^8l!Dg$goDP=Io6_EacPimj-CPIaW%)*l;%uX(C5!KPcV`i zwdp7}!Fqg`q$Oi6Kw@jMU4Q)<$u>5-JGX~*y?d}6^us&hG>q81_Rkdv{N0T+R zKEp_sq~f1ph2eLqFu0s?Q`m?!8NpM9%?G<}>zHLkh6Nd7Ne*_I176+z&%7qrgQP^z~Hmdj>FWSaFf-2K1P?zgF#R@DgE>Aoff1o8`>yj{$0k2n_z zd*8dJx|}`}E@F&hn_=d_s_@kR-|p9{#kr{XlY^Hp@D_piTG5kD0~z8XzX~qWuj=wf z7|BCWiX2oj7DC~O<7{3h6&EN_yb^)(1z>uCR6qXUyRYW$-Ka(PWcU_p@z^Vk(6e>K zyDB?90x5A-Mpk2NQDYq%)5*GG>`Jfi=DG=O`PFcip)$}AYx(*9K9n}KP1iL(Sq*Qp zL1ZOBuSq5$uNt)buQG{$#vD*8fU#M2B-gALi9x>m1M&d^X^T<)A$QIeIOP zE}J`P7R}W93#o29)HerNw&!4pn=+hXSuWU>VX`)tuvU*!u9-|tRfco?yVY)OD*Ld8 z@;o(^jB0%ju2_ScI?7s0;z$<`rlFLsCZl-jv-UKx^fg8nDZJ4bT|28aOee>@ znv=lS3)O8OtA>-wGL&3#lYcu@J0pJYNRKZ9`Eg$ToX(g?zE-DqO+-;)V~wTa;%VD@ z1Tk4uTMMMP2C)dAA+ERCn+|bM4qg#Si@>;1v8Jxt99fI?ZGwmC^+eR{o(iuU#`XP5 z-t7#%QpL$3+HDR<`O@3oKFEz25ox-Q?etX#`@#mq4 z4~xuxG>Z5%Jpn&rPRzgGP={~d#cX4-ar!`}{_lh(22w`%v3WI-qG`?KL}b=U6-guA zmv6V)M0gFBag`7HGpBwa@T>>$1;c2L83k3jxn;`dzYMuiaINWE&t!K|e05J=Uy7dwcheH2I; z&vJ!Y%r8fE;jA;QB9fYfc|x07>pl=W_-o(_+~jdA0=Rny7h{t>&RQtOa!!s`sUyM= z{($FNfihOoDz68e#^cSMrz=4NV#s;PeZ={#QlF*K3KC6mCsK|+q64sm)B zu&Hstt;8;S#yVd#8OS_43#^I`b&9pS>n;Y=p6YSGcz?QdF_1+!YUMnpo@v)tHAeHb z{|8(9HsDS`IO~0WnXjY~1>SBOn2@+zGycpJZte6#X3ruk0R@ZE1at}snd{vOZp@wb z$m!ul=R*JigXT4!uh%p8`^?7g<1+!YTUXqjwQe;M?5`{Z0b%@;?@G3t%C)Pzb)VIm+{o=9%^UpeEcO zS+JFTEfyZ~9(1V7;@LyJHqh?$e10ze7z;-3%o+!0mJ2Ip&pmS!cJZy-Qx|SkKh0{I z{Dh?;8&!l#X#NZp0TBRQ>}@fiUup3Bf^hrrHED(OIKIybivuzVyC?>l`Z-HqdCq>H zqsR06{RA2pyXMcc)UCHpz=Hf}4SlZ&@m`p#VIzI+=Y2`jjM~mWGs4eH6~{37QsQ0`XlwV4hmQWeku!g(5e z?8OG&$8N1%{pnGpvAYv+cNg<9G!rtSz30SNxR%bdgj{SM1zs#YAnW`6@EkhS()RIv z5LoqSU~Nb4=CkO$K_L7LyP0KyBe%=%^0{YF2mTKhILjjEjaK2_EZsi!8N$}>PZk(f zJ+7}Gj(av7RU+gGA=ke}7sZWxI1L!APZ+rPef_l9%_Is{Y3aK~LO-@|Y!?1#K$V1&{ zU)z%Rjm6HQ{w5F6SYH_}a=Xvh_2}}3Mp17~W z3Io$rk7_4%zZaI5vR1mR2&XmO}QDuPA07|^ZV%yJWc=BsxuhMI{a zSPvptf^&A>*;T(Zfjta5-2y{39&f2)xB6J9_sn~|K-PK6eJ=Iq+<$T4R`bleCY-^- z-C=<9?nlAW!)~m|-CI1~687SrKkh&Ha>q?qwdBx-Zw@%XK4J`42_@NKlB>bz{`bpFDm}miP-}G`f2&>bnsuGfcPiQkKr-E zHeiLZ%S!RpL$&iG@SySgpN?oorz3KAr^wkyre~j%kk8Pq3M@iK)MCgxv@W`+omTV} zi{_`}{B(St!?!$yzqWgdyax60`W(K0wzGN8=sIU~eUA)vUyao>OEKpsW^Pyc>;N62 zGQ{Z+67bC2#rIX;Y)v>5tg;Cw1rAkO7kgON#o%l3etzV<4~xC3UEMp8aj1qlL&WSD zBA~m0NACRhJ{G?YJ%=z1h;!>^+9SDqPM^#;9)>Oo@tMcD)3C=2r`-6ki%41LcbQwDa|`snEl{7a zBHfx`VO{#%F~5eH(|XitL|@U|XSm>iXZUBdK)dT=Zh6iv&$rinJn%Fq=XbUt;!6WZ zgLMB*0(-f~Aw<2rZ9c&FGP|0}%AFM0of0C{i35S3KnDim*jY9nTTX)QDIDLL*PQQ# z6Pjzj&!PDy(PPN1D4*fr@n^m+6>)Z#axoFl^#?7#jv?L062-%Cc`unc8EBoK)W>JmUxYNMf!7LzOjT=Xob;MT< zQEo(wF0vZCarvr{+{IJo)lmOfXdmiUvd6I5;I*+X?^*|Z9}o3pc(_&JfJN}!TK!i4 zENTJ!=${Rl@WjQ^V8EA>;DK2TIf2I20=BMavD?+YzPNcePsZGv_|%(70(!@|Ii!JG zkcB)~;nt_eDdK?l_LYu^^Im`6THG+i#JEGJ`J8EAbr=K>YwUcdtcQm7d?VC7XZr5H zSs=ZQn=R%>0^QvS29L{P;O4@iQdR8kSiS-)aw9U%|43ZV=lgGV`_DQr84T4sjf)wn zkh727Nh!j7`Q3;@Mi73K+56e{Ul)BL18)v?KE*hA4}lwRw>FA&Kaqi}$~tdHzx_80 zthWuk|H`2RnZvVszWBu+hpR{5b+O*U?LS|!##?`KzW-*3cC`)-Gim)#w^Gd>FU ze2pxIrkq7Q6%OfX(}*nS?BMty+{)etQW+^gF&$LY~MO&XeMZL zudS);t916Kan>{lHI%$fDgs)5(RoWF=fmmNY^2WHIKO@Uk#1j}W!q!fg^Q*ur)yZ? z{qoi=cNU;$hV;>7&x+EI-v!Nt@iYz$WzwBgzG5E;2oBF$EPK9(E-RMpMGv6URrcL@ z=jN;bW32u-Fp?SPVTghkJa)GR&*F_jj**Wpv4AtA&cmkruKrnYoeIKFot(mZ6 z7lWVPqno@>anbeObC-MnK64(Jc@4|iu`cSsIIWxw#@!8D2R2Ra_%pfF;mBG4C@}PP z|L?QfUj8P>0}>B8kGAJUpic7`XJ^L2){=qsfjTcu+`Hxcxc`1c5~6{u4~dH_q%Hzv z7bQ@8%;o~*qs{^n;yhxAd zJCn_it-of!jWXta_xp4A+rcA#8U=qI-)y;g$zp1Q+Zi{C`IZs$;cP#X#WLSEnmI{4 zbJ7^-=(CVL{dD%Hy(a(pD^h*Ow9Oep`{{7v>|-7fywKUqv5SaUXH|O!zb-D>gj<<( zH!s*XTOU0^Bfl3{g$mX^4|SSM=((>K2?G9D1)qv`qc9FJV?zz{XwFsf2fIPaUiL^7 zD%^^|1z>u0%b<&30B5ms_l&pg2l-ECQ7zSk>PWG(v;9hso$c+t_tgE1_qdb157;wx zdUh9|kubkO&ZF1wx&NYojq;V7gKzg$mvPz~S7u=KHu2}hwa8VIoj6}U7>(7qznuacp%xeb}Bndb3zg&+H9 z9#<58T=_GrEBxA?GKVMed`}|Oy`k>TW?lb0cbN?p*F77S*Ph*;4o1sA?ss;6wcJ^A z?)v@DmiIPiUYKVJyhr{UcRMw5wym$#D4nfq1J>BMxE>CisM_sd>Z18@fVDddtR`@I z3m$C%oQ-Q-&qd%JvO)igz)Iv}n8ayZ9kL_9yJ4m0*6znx{Up?$_WaDgTAK-s@g92^ zxmc-3i$ZtL!%m~wfp@(hk&@G>(#G6cWr4|L+$3kxZ8h1PN zc9(V*B@8^d#OYq235497nGm=5oR&o#tV-=pGvw@2;cN&JDzlIfyOBHHP;-LOW`-B-vv z_|XR4dZ$-q%`UXuKFB$}+z#{l;|?9>5BuvMxXR!7RsH*INR zK4VQZ67bnRAD4x@VC?R#)LGit-FLCW4}-uy=BsPrzW8+f1fS0O*oB#wy#2~(j`vM@ zeJR~}fO}C)kFUf51sglBk7wuiY}s+(*`y!J!!ybJA(ux6-5Kihr*-i8V`uIA_?_3+ zI@DIip<+(#7%HF7-z=C?W0xzVf~V2v7ub+T=4aD$8~FR6XK7S!WsCIMUB!h@UZ~zE4$2=z2r~#?12<`1*;9Za$gIxRxQ8ZLwD$3_D+_$7W?Q z$+;KtkAqx(Irjap3;(+CUl#sLh$7F9hyT|haro{ib{7f@w=Q;^^?2?}9CYS+rm)z} zN7oA*x%-!Lw>RZ1$0+dm`QLe8qHuH6@67uVkvh%Vt33E3B4-bIo(OMycoYx_*f{KZ zwdn4uz}ZVEFz#CCJN*oHqks21o}=I0_32&s$KMV5&D?h|s_)>heP>{p#vy&P)&}iH zpIMB8XSIvb6+Q1F3yk#s{=-6jYrpnc-welD`1L(6s0p558*X#mD!SIq$6{c|@XRmL zo$}C~M#g1zzF&UvJYN4XG?A}*#Y2RLIU50ld~;uWPluWfEM&;~GsGi2`)Q(CFiWUV zS;3z&v5RRb_rJxUHFb#J3+HWT?q}WD9SM8ly;vph+&(-yq03vvK9nEvbM3y*x%Rb-Z$@s{<6wh|z#Ztb zBd}X1k;7$s)(wxcvW|tL-yLWcYu|HX#LlWF0TGG9+UJ2Q!r!8q(@P>$9-(f>`+YKJ zl%194@i!Oi>;`WY_%k+kHVzHGNEEm|;&3zQ-eL8p)jZVQ20faP;Mjoj|@K2+Ab^LUS|uZg`Fupl_(efZjUc%BhE zv)va5Zr{kw1ngyqioi6>+>M+DUjqwz!dGeXw~x=c`#eV{bGLIGu!6pZVI1;ewX|&)+S5R=o%o{JfUHAIt8G zLPVW~+)*4d*MM6~J~K}^Ulww^oc3a(-Y@Zd?av|?oVy(?+)lM_b(2tU)N?T)=X;1l zJ=I00Lt{4{Uw7Q|(#|~&v(h}Ci7|54xC#*p9?m7t7GeUET^!>kE28FC?5C=ZD8y7k!koSuKsxU&7zJ6b+)-j>`>=jV8J5rObnh=*4wl) zAeTh2Xgwa9IlVjIeY4qp!kzG*lgGH#M%-NLP&*JfpO6adJS5az!oh?4c`@vs*MtU! z5aMoC(Ct7Fn4PWLiy-Jo8T<=2RGE6d`(}|DDgr`)xq0L+{)>A&L^$wq`FuYVvPDIx z^wEub$MfAcv)xC7ciHp3Dd(>e0S(lR(RcG3vC|SV)R2b(+xR(mp9~Qq>uxN_`2?67 zH*p?37&zjv^V)S;`dQ$^>$m%6QNsg=TN4gi%;$;Nu+vblCMj@n1i$-Cz#hjAvw_^H zGv9r)!g-QVOOFO*0drnq7HZzhkQoF55`nw_qRzX_J~0cJ&yu8WpMj7e4xQbG0w+2P zyAXyupGQ&JP~oIsqVvK)Z>9dvhH88TDi z9^yRe-SU3if3v`PkWdlC|9=!RR8Y`$-gms{Zn54cslsd`XJs2_OM?%EKKxw!?{ltx z7?@qT(?FhClJ&Uxp!3&hr=xtO0~o5EnZWSaZ}ratYayIX3f%5gZlz0i`!WHM*{$HF z!YvF1U!%umcG-I7@#a}+9~~+odR%D~{KuZFgSd0n=VW-av<)?+d8oqoc`GRToJZ-R z$lBdPe9okgLId}=$HIF&st&BpG$26w?Y~*zJyfVx5(SREMK_l$_&#(m%g(yH<)Yh< zksGxQ|MvBY=Gy0vq<#FY2M6|J&xaKUh8XWtiJc|J-5roP>y(5#hH`-Rd79()UL3I4 z?N{L<@y4xt=~g!Calo;FGmqSz2!#p*IK;47KsCN^mfM^I_G5FvKIX}KoEy?lc5_}* z-{PE?bT)JSIeAHeI|+U4#iK4f0>|?Qd-i=5>YP#MA@CIkfx{>Del2_PFOfh@Ix)fv4Yl5rUK{V|vsR~Om~W})V!e?Npl ze4d>=2qVHnmN*N1s36oRq0UQ4LlskhPowT%AXH=3eHj_x6O7jTS*ua-y!$HQGH|z= z;4O??d|@#}J*m6R#UWnJoR`Q)<=^}Km|LqK+gk1N9V`!IZ5U+F?mns+@LI0d*K<0| zE$M%mThcR}>(6OP6X&<(&L2zM4y1uY%P6Ro5h4dN9sgq#rMa>%2=XhH%;YSEpz9(_Z5)z)I?%ZQz~T700W z%VhKEk%>M!fC3k`=cb8zRU$pU$n(2Xch^yY>7w6!{)k|lCTo-oweBJpohI(q6$Olj zj}?$`6SMA~hC@ai4mlab?RNdi=McCUh(~~Nw}NHB{&}to?X)v>=bdtq6VE{k9V)yZ zi#d9Nf}7?X>K@k$c&*pXK#fzd4)*lFC^ncUM}&3RqfEdlzxSPJ?j8K>-oZgPsYz~6GGyO_fHcA(w+=eH*Enkn zIa{Cin58{Sr)M4hXY>P>o#cxP7jAFOy?}Es;M?m#6WH3IvjlyWFB6zay130a@aq`o zCj?H{_$-iqnbhbBqVfIoAPl)=+(o^FyM3ug_iGdw+(gib+O4TLSiQg@+2VJr9Wd8} zb3NEE9tMNn!y#t{IIoZAfwCc%P2Ij#fn6Iq`;@rn{yFtveXjlH+HWek)Ff2$^=zal zc<{ON5s<*Y3!U8wLcQy5$f(oQgUN69(2;9r(O`%_03z-y#vA; z1lAD$x92GKtT3Lz*!%uvU|L;taVguYD*{7SIhx}_{y?~p{s`;A=j|M8JuI36LsYn1 zxiNO=3|}*eyBn$qSr|Uv;(24C^EUN6GiHUg)**U`0y=KddBNbLs@AddA{pmhTI?~p zOXsKd7!4bC_S}x1+tL24nZz1Kfp3lkG^27FnT1*#D(o{~T`dl^D@4d%job)7o=r5j zgr~jI=9ci=)%JkSZX|&lLG`#UJolI9z+_IZ;gDApx!-7q8R)0gend!57pX=9Hb#V; zL*aU9d(jZttLb4JQd&E#n#Vx!FyhaSa*Cbb%g3#Jq!{*UR8ki~j$Cxh$BAO+1N1l# z&#KdJ4*Aq?GoC%fV_tdS!qLvoSPb>cJU<6_akxe2$CIJ@hjsTeUu|lBoB8A1W`0Du z5A)mYhrUnCf9LB!_%l+vwW&hX6^C5r$e~4Lz;W1c!gjN{=6L*%tQAdQy;LDK5(P$b z6OePz{fjxIdhBq}Hq_+gZZ`UzsA5KUJHT&`G^w-?ZT8~CIzmLuDW7FV%iF?eKCg^q>Sj|bOv*W8< ztH9J{ou!Z5`Q9&DkDp|nus#_JneEhmrJk|Dwop!wXm{^|F7L_nU5dbf&Rky?dlj^V z`nn7I}6^x2PN z#rT^s39R+pd0M&CbKP1=I3Nvis3j;|#)}S!jDA`Wy_;7VJALf4n0;kT?nZ)wB}0N& z*@oJIAe=4OW!R~nzvXjVGCqIb*B3$|0%x4f>Fd1efZwLh!cDjdQ63e`a~$8))oVUvk37p7Gf%KsDk6UkeJ)a$XP|i zy)loe{BdF`-VXnHVQ=9+)Iq2%?1Yxf3yp==Lai60AoF;^09_gPB9%6H(*215Tr5Eb zPEO%;phtT9is97l%YLWDEb4^E?$luJWP?sG`Wk#V)QglZr_fgsC(bV7E_Vw#1`6%+ zOft@!_c=d4_PH^enkAn1)yH)rw-O^~ThowD075kn>wY4frG^5Zjr_DVOORWE$n7oR zw3&80jtBOv$mzEzaO8PRvj$+E$0HV zEB6Do>vg)Ggu3R;S+|>$Uq}zs7rN_kVLMtv{mhv+43t=Fqy8Q0#qg*C9rxJ)^^XsBb-^lXyY3+j;&zPn?cG zj&aWOATY;^(5<4V9Ae$$0&T#Pd_J7WC~M{U@29^`-gob3S>+#P=H60@@44T8^q2oL zN^_W>eHv@>JL6C3!^rbv*|Xi46$&1mC~#tE7h$E&Cq>;Y&3mb7sM*IuhI>DIHhN;u zz7HR)4z&?Y;PhB@InUo~psT>v#@tvJoi7bK{Fw}+vQ9hwvstMh(d0X>v|Gjgzfs6m^i}M&%k}WJ+ik!P^^es&%>bAVYREIt zTGtz251NIa_?%|zeAm*QcRa9Nc}$JZc#d3N;zz0xJ<0q*XPx_+zaAN6LuMukJ{#js zOY3HqIB%eGvp0yN1wX?Ze>DGvbs7OXec*kK)afJ{Sf+hms`ub%7q#;`kvdt>!20=K zzQ(!#V(!0~`!D|aZl!a7!Zuhkvh|FmYCnM}_-1s&W4o0ZlDJ9+q`h?P9=H$$+i$^%&$F zn=!u$&u_v*Bf%it2tA*HaeK`(TgL9h@Ku#ysO5qJCfuJL{UX8SN&dw&k*CUG6P3H` z=w3ww5OOuK`=8I%^fgdXKq?}aZ-Lx-A>S=mb6U4yUuOq|oMsvru9fox`sz^B-TQ@; zLgf4#BCs#nzTQvr1U>vdXd2ziqor=n7M*uShdN5i#ol~`-dD7sfrA`{S}(%Q^LsHI z%#o+>d4hRapDkQ2B<{Aiu~2a*{Ntzm+uyB^@4tK8ZhOHxjj;CvHmLh-_M}`WEtD4I zqg$o_ZMT(dHD|x;IQu^PUG^Khuo>R`d+(jk`lneaUKP%he45mn)k2!&%eBQqsnn?F zcUp}`dA*X#PI{4lxLCTYRVVxg0Duva%l-xcJpG$8jA;&F$^e9n0g?kSVwB|;z#EUX z@1IUPEwmQEtH1ShaG&JQ|M&m=zd!v|-`5MZN^w7|fxWaZ+3dz0gT7{@za7?MkEG{%iixBR~DiU-t{G^CTVm!lOri`j@||My;0A(>^^N^nUk^ zpZ=O3Ab`mv0};f~K*=zr7*HL>T5$rUKvYpl*@%)!YK2Pm?>9-SQmzyWX{AvgSw{U{ zolIK&TAG&JsEL5YN7=wsuU?LbPEsc~MfFbAqqa!65 zw!oe~_~rt9)b=bT`8hP{GYQT}i$3kgN7tawkGUCW&#n{Yqw0e1{d;dM@cqj)l7KQ5 zK`9C0k_j7YAsC1upx6vM|NTyoBkV%?%1!vaw~rc}jkrGgzI(i+X0YR&`{bi;x6rxw z-fQ8rAKB0fU7Ue^6~4?Aqm+x-Vkjbl8Q~Tx84DU=ZliecwsKuQ9MP&ed=sPUv9Jzj zVoHS`L@JJ@CXi?WIpqQaRx%unpv)Z zq>y1Lpo+DU(i(+zEFfZo!yeiD^+IjfE&BT-Zy>7gjlHduiiK*iQ*};_r8K!qm!L(F z!G>jOjTx*7h>RjsX=??>YVh8>NYiHfYd+uY{!%Hq$!A0O9-SQ`tdZUxF2`p>8L7=F zi<1gzB?b^es3500R#a<7Y{YepDYIjVb4>ZYiNM}kLe;{3qmzF4NNnil`-EY`$39~W zwy&I~AAk*?{Y=r#m_>$T&IAz<5zUQ^BN-v8 z7>+=!%)r4P48N%#jBrhV-kXCd+{XKiSxK(B)HGHWDJ7JNM4Xun zIcM3nbB$K6m1Kj+RnlDA$QA0j4W1+G_)ADFO|J+9j z01rGjrV!-qBZUv;#C_hssd{Yth{GxaV{ENJB#M!-8XIe4glH_Z;)8FlyUzS6T`PTVwgTNin-PWOA-H&MWm?;k;0(FO79E`=BFu z{gv&54s60BkHG6${2N)&emiIKmM|c(4JJR%-6N^!E$ny{BSSigSC?c&P2i8VZY9ytw8e)tG$9Rq|l6tP-mvuc! zO6_cMI}drb+$*hI*9`f~V`oU%joF6i{yw~vIahP-r0$#{z6EF9F6Cr7Oef>6qEJ6i9xsJKE2*B``Uf(vXe(*AbDerx zsb=c6($00=9q+yGKf^#?v*|y>_@=(%qnJqzG>an`6D1;UV=7T>6^t`?#=sSCoD>cp zjc}MT)c5Ie=dN!fQx_0dkdO#M5&?^>VE`!xk->liEHD}VNMN6HA#;?lSY0&Mukaw^Eou}JFzQ_(v{hBER5I7*=UfD#KhA}!iGyCmB@-vJpxM^XU$LPn$g87co@u}i}$LPmL zz*mgX-En{yvnE&n1b<%U|35l>9sBXx2}d|AU%3$RLYmf?@X8f=KGhvepSXY1`v~qNkB4JDY8f_ z6+xs3WrHojmHo|)ArkPv9*^$kalicgQ>J7A&?1Oq6@kbQD=Y&sHIYJ*g;0=aFetw~ z3=mL$G35WOBjq=b>OUVVHkfe%V;BjbWDJ$ihzlm9)o~2T;IUS!$=7^7|EFO?{{GCk zBme&9)CFKQQbf~ODhLsPfGQ{%BL)jDhAtP_h%>DvGZq_- z^x!R?bAQ_DeKq;p|M~CNoksd=t1v;SJI+$E)7(?;_v`1P!jrj6QH z@RwHQ{387deyt|u?62EODZTg#0r)?Dy-3`z+`k()Nvm9K+qKZnffe~vBQ|D5_j5#$zg1tJJ| ztU#o=0$JuIkW?{723rNW@aOjzm$7?hqIKoooVEavjd=u&i834)R}la)i}OF| zwTYI*pvI;xxhP^P&S0j@#u`{n6q77-;1h`iAFNb!xqq*wzqX65N;CcU^X%jrBCV&7 zjxC|54NO}q0vduXP?2JUD@Coa5sYIDMTUkSES1~jY=|J-Z^sAIX(cHyCsR?Vy0o@kl9 z>WkB!s2Er-k%*ZVv4Tu+W-*Kalu={_9-NoaRXde(?&tKrnKa6|W}#K6wR3;_+uyop zPusm4{^OV2KSof1W41S;vK;d(Q=SA2P!uaFlxBokAS9*)W=C+hM5>^B$*OGU?a`o+#rAcJMSvuHRqvNuaJI!jNP|6itUSoy@Yd7k-hNmB?)T@YZfR&G8++J?4DALyxAt5IB@3MpWtuX%$gU<2Y8x z5X8BZY;YFO_l4Z^MvSnZde2V0ao)@KL8M}_znl4&W7mO5e-GbLWy zXq8g;a$aaQ|L&)Cj=BPqz z81={FhsG4m<5#CG1EB?rD8v{j0u(U>^C)9!%6MAY5Q-K*eK&rS9_9t4Dum0+kCoMukv&gYO*qB;rD4;B|21YoFVl&tt za~46?ME)YJRevpBWH+t!ZwLE!kq@&&lF+S;TQDpkB^Nhnjc@Dwj`vqmRby9%!I)PA)>X!ifd#U7*dt!yZw~5?niQW z_MV*BRtxV>TO>+K84GTxfHEcw8y&|{WRO6F0yP-??X08`S4aL;%On48>w_bxBTS&u zA`1v-j6D=E3Me8n#&$4l$j?~2Rs7qi_u(I&oeXjCxP@s8Ceo5H!?6&QWVEia5Lyao zg|tk{mmJx-pj?(R*G5uLk1F+2<94apXs4S#iuv;hTWW+hCfsZzv^;GAbD;r%CW4S@ z3}D1#C>S%4QXNY*Si$WFQn8z)O2c_-hrTzaAYW*0+QJi=J_7^+2x%Ol z7(`_mSQVj4pp zGbFf)uriSq8Z!+bqb#B?d)f1O&-Y(xXAJA^*_?|f<|^r5vR~C3>0dIw(u`xY(9Ttc z@vXh4(Ftv@Nj^30Ingn&Am)+)LQR$@Oi+x7h!jSez+oV#+(>ZHNx`7gherx+dB&fi zs5X)dP84QTf|z3lB_F~m>73`)^jGg0ddXY==)p;x^p9Sjwmhs+9AYfBW)uOT7=%7=XT-p6SN;qu5dlPQ50j*70coF~+D`g>8S*aG7F;O9?)2B>~l%P)rOPhUp1&m88Xs)za7Q zKSo+D&)=DJr95wG>H=dZtH-L8)PMnqv^LsmEO5*a(l5jBAE9adKSqnEK7VOqSKafb zrY^HsM;cpe7)F{C${E4{6NVxS@DSSCthcw`zfmZa%uP~HH!AHksVA+MTrh)Qn?P=Z z8=JP|2xB4(0dWS5p)AIfLuO)uOiW*jOOLAGU!&vSN6$+VYXBmKg+&m?hAV^_H<}1S zfW?EE7{akIEIqRO#JI_k-S?(FbZ!*3k)b*ki17$#_zkMLMJz%>47TnL+%+k)Pgfs- zxrS9p9`M4Lf*I7zwB@7;#(-hN8D@yZ!bB8O5k&|v1>xXi!V#kD6UWfs%i%4x(Ge7y zq3lM+l@J+HS1}HZq-sdaEH?`HYr;6~I|N#@glWT|C5GCFQi4%zW1fW%8Rroj93B79 zPCGej6`IYY^*1=e3K;PGxXFQiT6qT<2})GV1OOCE5fN^Hlnl@~gA9UEgVBE(CZ6Yy zzAEHAeJd_jP?>CRP(~rmMW(bWbr+&IO3n@|$!*fg6&tl?A+5Md>^8f_=YDR~tM|FIaFw)kNx7U9 z)9kII|C75OoKqNr~m{r!GAvmksMyopPQV@PW?xg#-rafpXaD*AthDQd+hHGmT z10Yt?LPZCAZ2HC($Bk#cO*C`*Li!*SQqMXzJtnE=GEgRSNO6Ou0UB_bfyMwLP$4EM zdMQ@pJl1E8Rw-$9fjeMiB=Yc=CrkAl+~m|nnT5D00#b2kfQ*nd5rGV2M6*lR5N*>n zqKb`ryHQP+s*Uq5Ff{}0WGXjpI4#^vTDhdvYP7lx3Atn>`*7lxCo~8r{@%2wh(gJg zwV6wib3lz1#&Se8(NM4#R{KjnvdO$-)Ax&MP9$Q4B19E3CWFWdpcH@_N*lAmaLA0? zYPFs=R_zP%ucr@Az@hc{^{LB4QA!nvjF1r!7D>hlV1O!0xCJkTLZ7ej|8dr;l+Htt zw@^#6GMANlvC+y*-LJWS`-i{j{Qf^i&e5H?^;cw3P5jMiPnTqpVMetwg2w`|nA1pW zZX{FK4!N~ux6BvZ5fix}raxk(Ei}TNQW4-L;)qkOj1m#nk`LankG$<;B~bNwV{$+L zyCzZ@1UQeff~{H*jUo~W0u|Mc z|5b0R{gWM&8xi;4=lKcn|9x7Swven+CWeX`9_tv2$Ve13$)$)vggAfkP^l*A_n>g% zPQp>tS^w82JYM=YHg(AvqBBx_4Y7#~A{NFBvsf7_BBftGjxz$E;Os0nT6S}Ea-nB$ zy@EpMSzA*Un+1wRF|q>b*it2tvK({8F}7Mz^pcsCnIn%T`(=qee_ zhwmlzk}6cI@$jk&KRz~+0DgRBDx(%Aj2KXiDe24&WU7*Vr?SI2Ro2FezUiGUpy`bJU@~+ z2E02Jfy4-#7y}Bm)j$Abk%CMrz?F`(V9`KD23U);&;%Zm25stN93lN49!(H^UYv>?Y{VEc5l}IPKoJ62 zWJQFtx(hK8WXO`ZONyN|(S@`io0VZQc=F(AIS!J!oh?p!?VQ5BE1Z5PX zXs`n?Y~#I`&xpP8MDYEsX^vaTI0Kb35Cf|*K{^H$NfAX>5o^)ld&W+ya#Ki$9SFTA z#+&%?(liHcsGw1ViHG)EdJ8?zV<0R^q5nIEg3;cl~3@lx*W-Z`*b9ny?~ zxlqp)n$2pZ=oDS9({}J0mvo+SFb{Rpy}IpDr2btBZ}7qCP7?&$nkXYI5IB~SMkYcr z1+g`P#a0bItM;=l`qr`$79H+6b95yo?_vzUYbC{`a+3|Jm0Wj{b{ag8JrMOU3WK-& z$aoX?mS3Lk3?dk@h-eWbCJ2EPlC1myj8Gg$gqXn=na@8cRC7D6Ml)%pm1Njd^nH3f znf7gB%EHO&1!kF$zye4awld>HSHwt?f!_w3Y1{SS8_#^{|6XV|+P_w-fpFQo(?k8k z+)bg{NpiOrmEuM2wo0Di^i?* zeyNeP>wfvBxoT3l@gXtqp3eQ;saKPBJA2PR(@Ju!PQBjK#Nn)ObT`8;yh49)%6`R zzoR!X-t3It?v!P#3D!W+nCUo1AR<;HkfEl5k%*ANQQG{UhSC4I-;O5{_sWz9YYY*P zQfM$i#$p_06;gQ|V`j6A`5}R$>rSPWlyc2cY^T0Yk0*@2O-xxhmI7jc;ZclbBq9hE z)|y}`IOQ4+PMe?bpc%$+I(d`taz?&6AK}w}5zMTygep!MMO<(!GKWLPcpVdn2U{|W zeg5Qf2RmBso80P_-OjC;U6bqc)R5VDO{3$@$7{Yc?HRG$z}Pawfs(-D7>kHztjdUI z1vCfFNXE-w$oU-5EH}8&XUYB7A;KT={qf{HVx!X)Da8aL4V6HGVJUG zib!YzG2tewI6T;f@4Mmu_y7FApYQqXhi`Cfjo0=YoL=L7aJtjP0E!tFS#BVg%4UrD z5hVa?uH%d%ez^B9skb|=r2T{0>Mn|f>TTh^om~>XUkY+RSHi`zlCdSUb6L7`QW|!Z zddrVY;Qf1}FHd;}wX$4Mz_ry@86I(N43ZE5K(hZ0oWXw!hkdQnPP3bRp`Hsktsyfq z?4|J}JglYZN=!hl86pP95@aleQ6zbm7fJB_GH@R6(5}PGzpuLM@mQ{rXh_dl$_ULbSGOuf|y#+$FV`s$QtO-mtEge+!4OF}TQJQhaA1Vl#C zAvpXeK`|E-b3x79i=>|A6C~9}Gjj{}#*({rptYei<*#XV%xLUuJ~iD*i4oCI@{F1s z0R|$eQ6w1Ult>`Vkh%(g`qQ8Ol-o^ejho;BLuTmhJ~7^Oz1?@GJ$Dg}NDLLBLJ(|OtTPBucJax0qX$DBvZB+@qV)KHdXki~ zVI-wW`q=7xp^)}SEsQ7mk(-^eV6&S)FdS206@YQ9fiWbK0;33#VZ<`N3P#(lhAh)K zS8SBBy4~TdZB@=M(p<*6^a3Pv+=j+HkH&0$y3$rkMj6Gs0bFDv&ft96QjS^7g(5Gz zU%Iwr*Gw71-u?c{>&)2i?Mzoz8e)*8oKS5QrX-3mCsZJ-MZ_^OgS~%$$`3ne^J5>8 z`KgbTB0?yL42y)Jv1LF}X=yBwMhFZ!+?;Z)lzXAVNvAYMDaP#++D;1Vdm*p=L zx7?Jmi~+=?ViFJ_kwg}0r6PzFp=@wu`2|_MjeJIx$-=AtGz-P6!g-QUL#z3lN}4R* z-sGG0W^FOM=q%kAYSmYL0|3AX$z^{70G|Af5XG3~0HzE;$QU3w02#wLw*X!@w9)=_ z+G(M+0ABsAr-S<>f3g|QVg-l3Zr|73pT!dR^G`pulQg4?Y=8YJmn(GA#%jHt7OK^q zBx$w3I(CPja=GNLSnZUOjY|EhZCj1nMriD-C!YH0r%J8aXrf@MN5z@f!?ofx?T1^ z|GC%1MWb=m{w@5pl@yBUZ;&tHCHTuPKmV5AHibEX;-UL`Y7*wDOgVPHE9SGC)Fj z388)x@;@KgdUhNXul)EITa8Ydv<7?tE^)ZTo`1mq{O3n)xmFg>v!YFfw9)#_KQXxk zer>+@y`h!0j}8Hy;i8wx(sV`;;M%Bb=i@wdHC9s5_x9)!YbE{FUcACGDCB&A%FF$AP{zjL`5`^82 zS#Ok*-w;|7+3(w(^FpiL{*5ewCHQm3gj{U5f8$GJiHKi*`6Xzj8{JniJ)K(L|H)R- zTXfX*srTxDe)tCai0|>w|NQt68#JJ70{hHEIP&_nlr5QIt@Y{F4{Pbqzhz>&Z0EyD z8P+|rgqP6s_Oc)2HtG4?702VBhko^M!FTp~ z3qSlkw6NH0CG8|#3`%Fv0)|oFF1epS71EOrv@5k{HCenVv<7rH$|T)S8~2%jM`uR= z#WdIfB3Sozk1S0DmT|B4Nh?xpv(AVuS7e`_2uT3Jp`Nh zZaWo)tq3QrtMe0CJ3S!yjvSV+i}j7WBeC8*$Gdbxk&~0#zy0lZ_wl5DUa2STuepC@ zWBi)?^MCw}ErBKQ=ihzr&kAQrHJ^F%7ac4AZ&7wyqS3m(yH;IY+3h6X&To$@>Cw66 z+a-Ile0q!#mv=N)No{Mndc9I^R_nX_!t7P}+Lf)|6b?_$_P!i+&JIslsd>XA+eRI_ zi9}_~9)26zb@9CGSyh|GEL1&K-%r8O`NN(md?_3hOO>^bTD?B+T&ng(bMxr@pn1N1 zcic!@Cp&wR;tPqd8_li4b#bkE-vSqhXV4}`rTVp%nC8m|>G65I&fBeT=i~Oh6$C!w zTB*Rjjb#0ce%Pvi!y6lU)xOmNJY}wZ(rzE2V6(_ z)$7dy+}YfDpu6>z&PuyzdH!^*y_b&J>F-9%IO2XtZoZveoE18P)~_0Od)K&}*DAlK zz8JH8a9YCpnjgUcD?}xO#TqIVhmcH@vpJb-jLe z-yXiopY{AAsWy|=Vzy!`p8jqzb?%qUs77>2j5d@@q0jhU=bqgk<_}j2BlJsSpZLA#IQ3m34T9UN-8-(9?_@9GH1AT@bjF(PY zsg5^&S9_b)`-8*MQG#g8w0DjzFwB`;ZgNE6(gPqp%BeWOG# zTj};4i3^-%=Nic&l-W&HYaP%X0OiQ83GmqwUj!nyocf_jjU)?X~-r z)y>)$eX?8P4ZL%+A}1QeO`%#TWvAL=rIgfDXFZHH7E@yIfE}+Du{zu>Uzy#tBmF>1 zYq#r3RN1K2*4k&=%N=pMS8Z(V*)&}_1grd<9mw;u=(@g6tM~M5d1I$U8po&AJ5`cf z@%GVVr^5HK_$2nR#*yS7ZeX#&n@vhfTP0Ie_x19nIF9P~2kQsNjRSLAKE5uTrl3(f z+^*?FG`9Ck>wD=6S+2=j_Hegp+Z3D@?mL_16}?{Fj`qtFEv)g5f%NQRtFwByRXjXH zu(?{V;?up$tM-PvIWL{wi}l+ze%87X0)IhjjZ(UfZpkKQE11N zdT`V}9!I!*-`CT^-Qs2YZmbnlEwM)Ou<~v5w0&A`vgT2Gc3Ifc7NwPnIyuqT-`Wo= zDLp(SII1SMJEE{xLEB{^)H=}MI7aX)vK#01jbiQF`u+ODeZ8E>QFis%2l65qJZg6_ zaM8@M27ht6dA@s3FV1*pZSC@)SUT#|j}D8Cia1+tt~Gg+ZdV~fVyCgi3ioMy<92KF zyjCiw+X&y*)y8J|u(p4Ds*g5zwyyIU?wv5OHp(&ZHiPf!V1J*^=6YUWt zo%>cf?WE*RUR_?EAAt0>usMpAIz<_{DBf67_)@vP+~;?-ycf+{VSlsIshQLB z@;gYO)v2eI+IaU8tM|6gFOvGoer3OrCMy4Jw|axFvA)_v1#TRX*CfgX#|N zZypwxkG`dwo3(Gny_>WA+GTSqS$??VJJNVtP@zTyO12$@g~G4(ZWR60OFYYvW2h zJ0izgC#U)Q%Kpyfm#RLj+o*b!tgF?vhr;UBD!4h>=22<0c}mXC$-Zvcgd87l*rN@& ze|CI;qVsqMe=8R64n^&BRjF3}vVKxsyF6am9@*ea+Gm|gwUjSZ?iQQPA|t8Y!ZF*_Q z#k#Zz*z)o5*1o+qC&%||TSpggxpJD6hMnvXh0R=ScE0BRo9pp^cRy&fzvlj1p>p@% zhl~p^!O_OW4lhowV5zcu-MWb7b#u*>F6G{C{>#zfZd$%B;xt~Ai ze5p6C;x7kl_2lN^KEESWKD}phI{~HJPO{lrzQ|XGj*E{wu6n7}sFW59t=gD#Rs@}6 zaVQVsZ&7JqtIO*zx25~#jmGU}Dc#L4HxxRct0!0aNW&GlcA~&_JJHb>2rHXAsJ6SY zC3cHybGLY@$jN50sveea_wvKm+mktSJ$^hkKa!WmH}c%w-BM6#-d`OTH`?h&a;rwy}O!s;JF$1vXb6BHr3Q2l4ve z$@X5HN9z~EPJt(LH}?3(n!OvfbMvrw+1yz%71*wvmZ~?h+TOW4u0}^^+lL2nEE~5+ zopaJ!TRCHV`;M=73f1@vY!tU{_G$U@hTS%=j#(pLG0ijGDl`629CkQApS^L%HuB7M z9#}p#_h{AD#O}%V{mpun^M}&IY5CTa*H3HpW1%i#ymFcBEpK&}H#(adUhVRGdX^?9 zwcG3U8cZ*FjTF!Fg>}@d z3aW6mKgO=R8)Hr0jk)9M)h!V-v6qVY_Mj1exxBhR00Nw~I(!3cNqlc_WGTAhrLFei z3O}oe0xurOgFRC;=6t`29&qW*=;gKL@(w#Zs9$}nS9g=4Ym$#Wwo;*WTd7ZQTS=U!uz7=yc5REStY02C^EBmarx1QS z`&K^KWBKB)T3^0Bxk$I<3FlX1sJN%ci66)dqZ@hZPTG4Y!AI$N{Wdzfrug_c-Y6cj zwTs=hx#kyF<#Pee-eIA%+Inb-+iiBarZ-RP>f6~$4K8g}?PzZLIJcQ4O|DuQ8KZiJDIJZlA2y)K>a>%*Nr zvwYrAd;FVT#?f+z-xo!7e`QO4tF|A~Z_I2Z>AKm@=g;^(hMSumbXwRstCOq4_Nt5@ z4mXZZHZIh1a<#0l$53%2jI7*BlC$Vvh7W4?CW%-*4igzyLM4NNLN=9e0l_Gg>o^4LRN^SFSGBI@K$({3TsqwyWE>2fOi}eK7p2 za(4@pv&(8{WBamvn{Hg6fUD%Te7UjL+Ggn%+B{#q7^(?|Z@J;)8+X&K33{~^U&4p0 zv(4jlGJZ0=XY?fQ24xPopj51QL>w-K?4^<4k4 zRV($*-2}U?a<+a`yFYnYZ^y|oFY(oAb0xpOSw7OovBjk`a=ppg_g|t`0#1(a3t!e@ zF{$B3T^yg6_L2FvyxAca-|kN@3iW6|`Nog0mYd`iYR-*g_}E6Cx4gh-Wpsa+)Qcy& zzM5{H9OiEzWDn&sJGkG!Y!H*0Z)P}#laSOIQ(*mH+BzkzELN4 zrBlSV)*tFDqQg&=;X7{V*v6W*vz;?4S5DKjWNZ0StskC>gXQbx+XgoW9a7x5X50uW zDPFJM$(`NW!Tre%(#(R+!78K2mG<)X2E2%OXl=jMsGprTzJN1Z9PchS&bUUNF?D== z^>AMQQaYA9ZG05pnlI}&^;Vf28Fd?%%9rKjWTm;89NrxnV2Vl1ZZ3|N_b%G(wgOg9 z%=PBp>O*PU!sE+t`OT{uEk&op{R+cw0)xj@DPT0#gq_@69_#Ck-Rm7)MWvhFZGBR! zT|AK1PI9<@a?@;Ftu};68-JMkrcCT*N z^P5K}knfM7+_DajHL{VX>~MdJn9laf?XkH!emJJ}{hPxR(vD1iz4Z{OZN=}d%IVqJ z`tFIW3U;$oIcT?+uaZr(@ojUvzOlW=uW)<>l&0xveWiFj#*~dbvTCDIKOggS z?5vbwT-#jc*XHJ-)OwKD=V)(rIS8`vN%DH@DiXtA^sIyGN^ZZS7{O zba0s#|9|q{V@GmqX%pP{Q{3GT3@4LXc(01eNvEb$`{FjJrBiG4!w(`dvnoqPdE6fj z3}!YNiIDakvt!4!m5gvTabsbcp4WY|0wmM=1lzvT{8@ajGjE43%E@<3h@V_rrXrfP=tvK&Dv9Ux&L1_^|Pm>DiYopn6j`KwZTuyc{%fd>OnKf(b;H@rt4-l%D!_Fqz#l|^8sY0s_;@`})g(R4yDOTbJ9yG~ z^bit*jfb0NCm1Jn6T-Fii*#-3!?reL_Xnpl*bXUV@6ObOt0g^z&BtOu6nIk~Y>3W9|- z*}#3}Xn~w4c&680L42CWKH<#$F(Uf{wwGC#$QqU9?vOyMbF216x)BC=u`DYEpUh=k z=?leLA~H#B^8j`B;ps3Zt$RgPt88b1lL%QnAPKj3a>x>qxY$h|f8zGNj%_Tbc>N^j z5HayNHMt0CZVRK~wbE%Iv|@EN4?;RF7Phl`w}?Z7q*8fG+E$BgHGyeuyO0H*xxo6P zI7bN^ZnwP#E#`S}+$U_5?=*iF-^<*?X%{ojo`SMMR47zZ38kZn-Zot?XJD~gRc2Dm zi~jHwZg2xib-Rf1HS0-BB?z)pLCkSLrlisyh?v7O->B8?6aL(1@J;9i+CRm;bD8jI zKL2U3oPCtSjMhWP8D#?^Bg^J z@xz>>@!m2R{0Z*8Q~Wt>FLUm4?@>4S~0&lI`G4mhf9cJsCS^=Wn7C+^+r*gorp!())x{jox5dBj&3 zV2X1*O^HqPh1TbA5N%I-u-;l$GKXlmzd-ivvJ?owS?CS1aVZ(r)1TF|=)?<_h!n!? z#((UE|0AxKImWvL(5?2w`>a9tIF%y_M^MV%8Q>=4{e8_3NCL&(5Eux$bkD1?qCq-C zVi^wWcpG!WW)_y%SP@oiJ-EQUju)SJXx#v;E^MwSJY+fxIB z*()c)^GDd_ttNA4H|?$xbH8bZgl13(5{gTmeOV8klVio?|3>)yJCeV_ z_gSG76;0bNmwj4y7-_DIvvoGbyX<7Gca3v*t*;(rWV9EIjHp~P&Q$ac%`L7|n2s^j zqV^Q&H*LKeNOaf_T#?hW`;PO8ZX{K^xXk8;5wgK4o)xlZ7^iZltpr8 z?5Sxjbf&5QMn%Ruj*qdu%r#60a^sGVJd+~VUe?{Cl*mov44P_%lUpPr*RZa(XgKHf z;6acYB^fnN&9yftvymz%*)VND+;9c0l4(ROfF}MM*^a-*_UTxj<@whu%qqy;)zr|P z0ES_kZO$In%H^{Hf&w@_H#){UR|XwpL4@Tp-@Wt~EvLZ}BYWNWRhi=w z+_h_LKkD}BPZ#PMaGAhkn{i>At9zl^Lmp2_b`7;ixVAc#B5@AY$BfCOjcU97$1mDH z;(DE7Vt>7+Q&W4R?;VcOM)DKbgTM{Og)Lhl&Sr}VL+b3Vm*SygOl+}NXBAz0KKolh z1gFym^THPofW4HwU+rx;{l}T9pK)bP7ru={`LdMXTKnLy#8>JyRHQFmV#5KB8)Bx4 z1zcIH8MNy|8mMqwr!CFu#~9q;!(^gt-iYflKJ-1X3gBXqCu#So*WBmJDdD$5g}s}C zqFD8Q!Tv#m76r|q&=?Y{u8swlSJTd#a}*|B?&6Ut*9bC+;bfvRv=+%U zAxMuTCdX>2l6*Mr@VuZ^E#xt7fK}>6AuG1376aQ~lJ?sG1LQ0GY6}HKboGeGxpAr* za{xg+r3g(diCSXOuzBE_Ggoljs#qJm`WPuhjdLo3*2<@JsefD#@G4i*b0apFoH4AH ziP|H7)lGj_N&RhZ!n=GBU#YigI6r;uZkJb{kQ z*ZHvnDN()b(C7hnQ{B&{0b$GuPSkEv8!hu+rL7v@MkH@(^WMn%`^m<%Br+MAg@~xh1*lt@kd9dHMpuWtWL2 z0bOnfXlPDQ2>+pR_@{pDpXh(5b4|Z2WBEP+K^nR^#R; z0tvZNjP$g+p&Cs{vVs@Ib%7_zht#(Hu%h>GsekVXd4~O$x90iy7x*jwY88CGqK|uc zvm7+53_91Qv1IOp;F$_BEY}^v0@d)qlcRi9nH&^(7AzZOkxs~ut5U5&&pxR0;P`x) z@(eIGBUDMj{3{Wk$1qNRC*;VhwYOSLg#30y!W?ei^8k%=YXwfP*jn){4%vpa5(GNe z>5Ubo&pGMz@uyMS(YxAfX}0gX?Em`T|NCG6hA&_DKkLaqeEom?>Gyu>Bp3-NNxCy} zxm}|r6krmw1Nx~T&Jg>Tp}2$!6zV&W4~MD}f|oMB<9jL$Vg(&ZGDrwIwwyKPDV>PlvNNMDJIvt*%G4}BcQRF`D z1?MMg9-HX?bn^eVpVu^hR$B}=$ZyR@EiPUVr5Cj&>Z=~rA+{sL{id~Ce@KE=%0$-! z?#6k7gyt=rWY@+Fku&PtnI?3{7muB5Hd+xiX)7Q9ojdaOtTbf=X4BUx4%?(0-!0u& zmGXdK%zz$GT;$CsqRH%t9zM5KG5MA#u!}p_);+?>mmXdQwA{NHGu{ zGVOc$QK9NF1dGaK1oDyT@h|Gf%l9+kb2$IDX*jF_27L5n|0UI1 z*wpWa#*21nCoq7sH@O$*W}R;xCB(e2rO(d3&gk`~U&1Jim}e+#{;gx=2O!u7)Eh8` zN2!pLN>dh~W{H-I`1Axf?b&wmhh;FoQT=Uh8TjqPm!CRJzNBGPexGOdqNyQi+iY+G zAL@LbR#7;m0kNys1z?S4o?;5LfuwY7JJL;M;DD$drQxzPBH?kw#U8f7dyd(io}Cqi zV)puIBEkOEXZYtopu~9B^GP=yi;SLI+!8a;D2k?0|{(X*gB^iVoxeOs6O4l7$S++e2E{BqdQuys+8is8AhvKJNL5HYv05?LWXnHd$44lBErb1%x67NxDzZN>&OMQcv8#J2?!4udx8s(mOD?BMTP+rBvlfo2R~!g%Zl|0Qgzt>u-g)R! z;=0p>O1o0J(9$32te=zHe~0C{OwE4)Cf@c^d^oT`Pk^oNYi?u?H@lB2VG?^XH@DTA z^X{{_dEO+VEiwkX!C@hH>E$q_Bk*F_Qsk|;lPqG zTmf!)&@lFgoGkipz^@^BW6Y^u^*G~zIb&!?^ZM}o!3|BPjKDZKD z?`^5b8}04RNa2}?YSTLJ4!*XIBVR?ipt9!1Bu$4z=zkMR-)-smR+`qNpJt7o^k9>V zkq18MVFP;yf%C4z$HkkD*&soYWx=M@XXuOt0cx7Slg9*x>dUo)`lLep(Iij}KTL&H zN}d#LA5eooJSiWQ&HWXg?}b{Q#-cKJEtd4=~7~!T6)ERb8@@9>SUbo0>TN9+>5L+fS$3glBEFTw<{S}(`;-ybx34%DM z)H>2&GE<|w`dIeuxqHL(G?%PEAJTQd3Y}PuJQ=N!57vqmJxed1CiOMYvcN5W0KE?=^t;`l;y0jp`2AVJTdwJzHe``bAFGIfk5sx;q6uQ0W6F=Ipe70?la3CG zJAOoieS^1IrthZQ5wXzqXfL*-BNxv@Vv@Q zromxN-vXjc4}Ca0y<=Qhzp;DnM2)v+8Q5DDOCxpRH?-yKr-d*> zr9wT``vXfp$glntko`a52(nYE4v$aF5O@RHy=aLf55LcmrqpP^oU|G$W`Z)PB)S_W z$71vJV;%6)ZNHps*O9z)E!AcL!$-tQq+6*c9eyZUKg0O*5cglcjb(q)+|t_?xDAiK zt%He|?`!1yiof|qu;o~Mz!~q3xOE?MI*|Oji+ZlQ+KDSRVFZah;|Dm+!&)_|jho7Z z2vY8fET2d#{R<}k>(4+w1>u(E!dYDV0@s-l+3Yg)6tFnx3A)-vJ6v=a$3vpdgkT?b zvcMH{yTDs}pSFSzg~Dg!?0F|2F6WFF5!kWlVnoz$Iip`^dl%(f2= z%q8_VSZ+U4}Slix9Qz)XBaj=&(izD@qde6LSOM$58o>1TW{f&T_F?~ zR+Z9w(ai^fbbd+-Kp6J`;$*pG1xC(-+fbq7p>t+%MJooiwC;W>GlvGNYdu6#!pPUz zO91-Si}`Q(eKgp&@Fn#=z4R|)wSW4a|M6D`_G72Qr$kRU6-&tPDAEB-QR9iRqoek? z2TfxJ07Q@&YIMgAEDQi@S5Tp-K-yjTXa$g?E(10bkEmp~6%8h$zq(I1w)$Fmsp9Ar z(fi#(i$4tXe@*WG@}&N_5aP9?q8vX^O_b)igbgsC%wt`;t0z(j#>m>4ady#ptIvDI z%0?>@3OY5OU!@Z2u|iL*;mRXtBj1Em@wKR8%RC~o@*i*MUjq0xLhn=5o1)#~EW#f$ z<>rEP&u_Ab+YsR4-cXcqw4{nJ>j!Toq@=~4B-!pprUZM0o&++y{U!kwa4{p9sn}7Q({+=BE{@E>!-bTZJ|H1|N@s*FY z%kTfx-zk)z{_e-$@`H%iG}CW7>BU$+uUliDaJ0nl(RzRICg>N%rF_JN->VvshcMID zl=eLGbE535DkLuXJ2dAZFpwN|E-M`q$9oKOCz}=> zA7PD4URen|Cw7O8_|@c9|EN8_VR+vt@IE2$fBml?6W(7=mJFTQkB{}lH}fiODmSE4 zdiT8*9S3zS@;>sfnR|Fev|oITH1r!ylpv7!X~sVva!2QSXeZ$<*DQ5VZw>(;_v8HO zfPW_L=Sl7_7pl?LQ~`6(a}!@)M>rh4vC#+@eI{9TS06K>%rfZqU_GW+r5$lo!~w2Y zIlR4T!NWf{Y4uZK03f_{{Fpx7aq#BtuL=BVq$BnfdvW9ls5zl^2snT2%&n(_``%%9 zHATR|%-5sa?whNgMNlxTu09A_#yM$z1blVKFBPU`b! z0TrXV03XCE&C`QF1fe1g_^3bfKMr>Hy~SfI9B0RAlF(NKy705Znl3{0=5r}ez43)!($%E#lAHTzY@JY{v%(1+OzRu z#&bEuoPyWYqu~e4E7_{l9THO+isc6lonaY6z`bc=youO0vW{hq5Pk2TgiSj%3nKeN zLrr1W87lU4!uolDKgZIH5Hs^A= z^h}1VJz1Q$stok0p|(MPI(uyrNwUGQi#8Mm!$6%l*JT0+EP(@*N3DY|vpDqN% zdJ%e27_@eROUmc?dk(o%j$2l6FYfz(*g}^(Nu6jjMpj`1WYH3oK#&bYf8JTyKRP4! z$|>7<;(k+6LH^rU>#-*ZcQKJ@ub^1--Ws% zWnhM4|2B&FR35m6`StbqaGDDA75m${<6(O|!#OU#?J;|aep$VzS7(G6Yz8<$XX@&X z%E7R9agX?+b?P|o#0>GwO*KK~DCU8uXCJHUk{ozW2_I>#0eUut$xDRs!&&CK6it7% zNBI7c)%YVl|E~JKBx#tu7yf-ooAOzm?LZ`PmyEJQHw&PVhSav2$f4n?95+>Y(L#Hzo-Ytia+jWRJP%G zd`b5?ZKwCzqW5(~uT>VM1weK?KBS}~EsEF$P?i>GFD?!z((&C%4+U|9ym)L`Uu(Am z1Smbup%8Tzs|(I>eICuWD4wZjXwg7brK%rx4g5IPw@Ssgs1Nj_032rI=Q^lc50^u3 zQ>ip#2asY$m@vglqipe0(y6DM8nPeHXufh(r^LFM+)iQXNBmj=JeN|r4mNuXGiRpu z`}&ap|4L}LP3br_<@Bao5bP`RS{&#W115ot>6)nVz!D)_%pO*qsCuAv^JH~6au%La2%@;b~qSp5Y3n=vfK*A4el!zY$x8*XWUz)cqbZ`0tg&+tf zsMpSUG|Yp91cDYEwT(6lJ_$bu?zI>gbO}>sF=3=2LRGxq1ahOtr;!J!A6dwBPnc+z znyN?VBC#pvHmsL8W{no4EUpGhNnTBIhTZp@v&C^3jbQ4 z9XHedUQYKK&(q)unI1DcBjY_h0ZLGc&;@#fiO1!F8>Q}c2t!7dXGLO~2o0V>4=)@D zyw-)>v9xsJZ(Pk>ZdPRHpd??Cf_;2gO8%Y9u3gh{{=QS-tvdT%P+vP?dF?U$B91O{ z73oJ9CyUys^8k==W$=|Ds^qB*GVubBjx1pI{ddiJ&%fJH0FQ0Xz* z&j}6h(hu(;o~Cq)-<9xd)PQybN^vE_Fx+ZZEOZgBYV*|2nhJ-k=5S@tfOfq-uEoKP zBae}mzJoBeU6ClN@)&)btR;J}jkhz-N7s%%LHDD`6ZSV2#P1fNzLsf*7qeCyYBXhV zIyp$uih0;?hSHZ$;Ym$8vpHm=v}|uh_7R{*>cDPHHkfSVr;SbEq3@!I>*Rb}7DZ=U zh34)*EYT#5mi*oFBwwrwL)sV=$@{4K59Z49<&&`4r&>eEgIX09xkn`|^Gu%i9h6x_ z16gp}hW+-umFVGtfS^lZ56feYZIGB#P{$7WKWu^dhalLr^zTc>zQkSAjsNps|Mh?U zb2RG@Kl4EhDoL|&Y348AU(+vT@(cd5)X_u__6ta42NTnO;m{{o{oR)v;8yGzu<3uE3f5j z0}!T3G)^ zSE;058J90W@;Y;r|Bp?J-$*Xo^h*<-$Co!h_Y;>}1Ju+Hx?x0nJ#VQ!JMPAv*T`H9 zYbB_PNi00UMl+8t73@~iGgNXLjG1N0Kaq$>p&0jle1NVqO{1)gXBUuhUI{BF>72&% zm-gMfevA9PYIYob@{}CVj+p2`xV?;)m3$~3%1p4yKUK07E1uS}u8@?KhkSRb%Bh71 zLy8}J4h-B9Pa_Z+M5b`?Xjmv+NF@Kms!qD+mmiC%UT1uJXG4;eQP&<-M0*PL?ZDzl zCU1`>XL!&E)E39b(ND7x-+j)2lw{6~HfpY1=O!_oO^rl$BT){t)9w(<3OCQBeFh7x_Q<|wg+3Dn8J7}B86?<>$vdlCd#98pnIEFSbtb&zB8B(8C~v(ZDr7~D8f2Fj zjctX}jEe99!`J=}kYNWW+Vo5o?IoV3O&ZVR+s4K2OI(cIvs|9ZX|NGM zu*X#)z4I{a=HQ@=MNvgL*L_gfZ5d~#j9eox?t*?PEV}{ba-wFq=E$ftN)b$3%zW1oKg@0^Y-*Nkbnn{T7tZbIR%G!^CvMv(H_jSP_ z;wk$BW@*<4j{=%}L8V}iDOa`99%yvOV!^vBhs?)gsgb{V`hFrFeNiY~S3ALr6++R3 zTf8#2Xhsf&#Bv6E#XFU6@L0cqmQ}Vre<>ovUH2%#NkkNJS}qOJTNY09m%QnsU(mNcy4RWr zd}LhctGQ*MK33F|M9BaTlbthlNTC@Lv~acE*~Hek1nUWAI&SW?ULdPR?ffln@|rn`?;V%F&1|CZDL zroO(#3%`5xUx{CXi12Sg#6OnJ=V?0~ZT#+JLSLeE4qp^vw9rT`oa$gzImHZcJa0Wz zGl6G1@pRhTLz4q8A64Xe4!p{^T5-lg5TwJk1MMa_iPS8waSw~}#6EPO6>^I@U)+(E zr*C#Z8-MeSUz3`vR0Q{q3(IiM_}Y;vQEtlkxqzr8Tsa&b&~lbJp|k8`G9{+X^muo0 zVQ&i_=836eNJ5QlNM+_$mKOh@ci=*NXiWb`>paC>o0V_2KlLKH^~KPD_{uy(#hcv| zxmdN+4GykDQ==@R+~O5H2vr9f_u_PI><#;J&7M@sY^SH?N5&+Ca00hfHZK+XJl@9sU2+&# zQ5!HrSG-o`>3;McVBcx&x+wkDz6ZY+^i9XAhA#)~peW$eY6`pddeb#A=&XUqN{eIy z=aGrwkamUgY+2}2&~J!M2xf-~ zD75t~;+|b*|B0lO8_EPE@m0_ykx^zKjW95a6UQQkCX=8BYv-_R-%B!I4*%T(g=M9@$i&A!Q~|@2BY!|Tv)Bo~v!X@L zqMNU*QtzZS-@0($G!HFUTFH5*x@PIU22=qw_h%2TGjH=(biRKW`oO-z#NQb2<@BXp zp3(cp<5vM-Fk5DIPNbB5G*+L?cAqM)I;zDS5`%%heNeQvLJ)+hcLhUD z+29)a5(}%=6WYUjI7|}>ez5QDKb$@C)7fuR^ByF8janymGdBxm%!;um>7$8bbz;|@ zroiymlw*O@?I2>lPKc2+Rwlg^k!Xu>_j5FjyENrWtAa%uW5!%lrNi zR@0ZctJ8LR7wgxEy(f~BH8GFI&MBAbVCECMK*ZHpE>3cjbSO%L$S6b&tBJ7BrMb-F zj-BmG6z_T;Fmul#HLDUnGm}?oTuk=^DHfT9B#~v$*nAk zw+!!0kln)_fDG>{0NjkW1F zWIf;JGl{6i7c$}uHqJRyQT4JtoIdn9{*Lgb>!$Qe+OB2xR&oAnV!t_--4I65%Zjl{ zp9rgfPTV>%e8z)J-<}M2E_FZ#_!T_NQcLQN0J6K1uFVFExq4U|nGrDT+yV@u_<6ru zo%~yO+n2ek5a^^j3ZnViw!m$(&IYqD>@=!VvvKzE$+BF&0tRB> z2LwGNU4Ny||5?Uk_pR*V?T)_|1~dFEV~Svb3WhZ{ifbVr@VFo%!wgL<#r5P86@`_; zZpjt1k88(5$FyG)Na*&BY^@cuLF42lbDx5@0zOvm5az?I_t)nBT$XN9zNpRt@Ah0gLe{w)fxpJew03SNN&`6IyMH4<-#3W)B=47<)@QM` zba3|PE9%|t=7+7=hHY0S?@^f7vMZ@|@B1n?ZsB~tsVwXePMaH)e~nK{H9f&hW>~cu zlhi``x%ynTYJ0SLF1jNJ!6v*Dm#j5$B4<9LA_y7EFd|zBDuJ!Z)283K2m)UeBzGtDuf_guK=lMbe(boaHscL zq+)G2>?X(L3)4VgY)4VA-gZn|1}I0uysbuW|{<60*wghR;_>A6G@4kNsE&c3rb48amNIWp@2G1>@x za+nu%6?@;}?ZX=WpAD@ps@}2);WU+b`(C^7nnD<*+&0_VX+?OtZq#8DuH~R7hfdye zA*M%ho6>-CeL%P`s6kxG>jFp`*ZEo9@J*yhRvV*N#}wTZiygeE04Dx0ME*OX`?UQ9 zhF&&Mo^73)@L=FhifjoGmaEO7nwWsi<3cD4ROK1eZ}qGg^G?L}qZG;cNQL`doS@T( z#%K%+-)Q~DX`|FXLWWsx;XjV`{~!PQS6{~6dzSB8e&)5K-Q!e5rnZd7N6D_qYdpm_geId|g+1_`Nbx~*Gi&0;}&5}&#W+`%SC zL99ankA(MqONp#$Q8uZnS;iagrt@L!;8%6pchekC)0gU-h4&h{38!4Cp4Z_ETuAEN zh*#qtQu|b%VyIGxs$^R2h>0`1yo=7c-Q!&aP(#EIfr^uslEow@ThRuBn3YP;5K-># z-xp{7s=>Z1u7APA$B~BbnBK#a*jEJp8RxUy%Xa$Z@)de9+~B(dUTvnt-4|SieW6bJ zoVEs~MoE3BMNDF^oZo<4OIBg4OBHMLvI3w)+}_=TjMTYfPFl%yGIGh*zD?KG@jmYV z!TvEP{xd-2D~$aNuuZ2qO@1L>&SQvXG$WL}G%QX_rvSu{xw_P=%v@0R$|lO%<*;{E zGMEGQr@9(K!%TDC+x#Pwae&q?MJ9Fe!CYCb91aVkqCP)AZVdcy0qn>0ZDQm1VO-x5 z3$G!qO4v|2#>o(O+Ch=Wvrx4r7|1eW;ECYAH1>!|AwY;6ns+@lTB^+kNw1Oem`r>Q zk(WAS5}8%A~Xu>q)Qtx(kDw#7Ldc8jQ0q8TTvae8+Q#`rw_Tq^&u#y!TgS+%Bhu+6< z2)@8d^L%8ET>E=%j|bU)P_zyGzH$=##+)&o=kh%o_-e+eL}pH-AsxSss!rl$lZ}b_ zU}K~*G|p{j7kYvKwiP9pd7&+5s+QkL^Mo`y6;80d4_EAog9NLbk%56tDb6tR@gBOr zE1vp&|HG?!i>{(bHhlvmNu9|r1Yu}Y`P>W=Q@yP7h327 z(S^&f#Lq1Q&VAKxjiks@5E(cTkAIk)!;&uPmv|XZ3#6iB(g@faVfL{ z^OU+8!lctyV3sCAp;+-TQ#{nj2O3Zjw&)*bOS4o#mDuB2GZy9J_o3z(ktRp1NGnvn z{ID_p*o^Vo)q7&CDfhtmxJS@M(M0?rs2H=`#Rb7(@Mt3W7WFA<_rjh@z@=UZMyVCX zzg5-LM|srV7;R{72s+z3 zh}pWT{%nc}yJ8(*Su ze7DqJ%bjtv6@+1jJ(;Huh+KyUUXU=Jxdk`VoC?PU6L-JvG9ar6;Z<7?=MQ3B37ti| zrMeMJwh;c2qxL4_C`y~lJhkA5B`g2*di|~o>b(j4eFpkJbN`iZ{fCJJKmXzHpZ%*U z%fC4GUyzrJxo?PrNiIK`X}fc@9J(RuK0qmz9w`JO+-`UZWmnXp$nk7SX8BNYq;EmN zyY^O3CD~l5Xm8P?D?O%|ZdvdE4(me$_5U+RKFvpyewoTPZ_>GI-^0(Z>Dz}_L7B*_ zhgXZI95^GwOdxhTgs(h@i#X+sbAOUK-qUV&wAh8qol?sKB*7a0-UL?3mf0m>gx+k@ zbgSbbNn-NfSW+9Ww&ia2s5 z(R4{7!eO&Ag>W=);{EiJC$$nzT{)K^uJ&)mxT6@IDu7G>Pop}?ud5ZQ}&+2=)(HZkr?fh!FWBrj{%8M z(4cubb-U%JK%5O%KZ)h8PR`vo7I}aKZ!T5*65=ND>NEOnbp-LjS_$I2QEnX?3U`BO>En{x*Aaq|-0IpRAUct`G#r%nR zx7YFIArE95svJ6R7tS0_INgCp`kCHehXH&Umv%0n z={Muz#lTe17GeRlQm1BZ{g?>h`7Ew=OAGf=Ia__f+xVg?059#rI_`u>&gqh_gFCQ` z5gRfHqA53ym06^P@9vklt%YR!)k^%f=c2Fkw`Q5?6Y26PwGg{P2%B+r7SV1oGU_RE zAP=2Pv;Ahz$|Dvmy%j}GH%YcG9VXkL+lIt7vDdf4*px!n6pLbXpy2y9H=E)7jjm<- zmYaJursZ5(k)lF0i@~&JuW$o_jsf4gsa$T=VHaeZj1;Zc zwE~p^+U^iclOK~yL-`sY4W&VM3bSA_zG;z%K+(D_Y@17UeQRAp@r&)=FY)BzHQcS- z#wUzBY9H4P{{YZGe+uOMwHrGw?U%6cn{{eYak~4xTdu5!OlNC=! zg^TF=&oGkDi-W7pjlM1Ga@M6@#`j>Nd`a1TU=Irpcq(Pd2HbGHBr=`Pr)+$@`1gAh z{f*>49K)tbn|B}gHG1&|bC1WJB9sT%aQnl)6V;$l^p!^}=yp|910m?BniQAjb}=a! zFopzY5`hD~I2l`_$$3(GV=l(C4JBReIMHN&__$MCq{-5x98=O_o(Zqj@FgjuT<9xQ*FeyKzk0GF}3)n`8^C-M6{hJU4+&j0^Z^z}X$S zAzp&>hnqO1&-Fb-_p0h=?xeeNtSjokfb0zKP4#XOoW#S!y^Tn2Kb^75BNAr4&dSP| z*@LO1g#?fr==mH#wI7AcL67(P7|q?*3BUSDmHtuge(U3Vi{`^Gn|hA#sdAy{n=3~o zLfxF%H2gga9AKG}BbuF22B}5hDb`fivC#W%JvnBpAhWO(QZ-|8y-hcZ5h&NBfE#D- zeP+Jukxfe;j?-t74<1`txRx!hRMqAw!_Y)|Iuigz!R@;?gBtM&g>An+fK5ZeMwYKu z7|M>8#*Px(_R3?+sKj#kxSAZb>d;evEY-&y51)!4qF>QBU~?EnZ*v4c5s7ERnLwZW zdxcszXoB55|O@;o6|QsT4d)?~ppg;0U>?xTJs<%-V+!j~?`lz|&f!rG*~O$n85e z4QpT_SMU6_-+1o6yqO{U_cj6iWyvIPgDg+N#}9sl<1NltbC06iMmjZ`>j$3PlKFv+ z+?pywF(#4$C?zQVKkYqNld4L$=Xrhwbzb&NIgW~XSKV7hT{0l}Gl!maJg)HJJmh0d#2xa9O}E7_$MF|uB{%0i{n6zd1IRIQA<+kzH!(kmFV zNY^A07)plLnKo<3Vx}=z#99;nI2sSy`Ln#=9pvS%t@TO?xwOtLa#SOEu24!Vnb`G( z**M3g&#mgDknU`I$3}nH&_}aYYg{bSW%hHFAkC4cUBEMUG31iH?~SG0L?PSbLt-&v z3#05IzbK}-Rd=-Ebh&t39TKt5NjVpbhiUGT7=O&qpOvXKz$sM~LlA_a{c6(oG?Gvqethg^{JLHHQ4avjlRBfzVt8$NJrqtqtDV?=+WwtCQ z)fPi5(zMl1W+#CTurRuJ5QVP!ffAd%DosnSf;ydJtnDQTiMf@M(0wTYzFORe~>$^W#vms ziGP7E2+CX8?G;VnO?ii#4E1)Wo@{c7jo4C7iS$?!`P6xFN~_sStJsh_Q?f`;xoWLX z9ctTAkCaEr&QVxSSDofwXbg(;rZB6v(;wqnte@WSGwEZN;KB!7y`$&>jES1EQ`G%h zNE6e~@3iK(%jZMBdb^OpSOAeFHIP{OO;s(hm8hfHJsS{l*CAr&UwF(PK)kgP#}3y&J8ys z{lt%yu28Ji$-=g~{7g^9nJ6`PnJVAiQlGN?K&-21KS(r}5^}0UHio-+cysV1h zUz_hOV!8-|r|ngNV{yQvaBNZiYW9#i&aSmfb6-*0a)nFD4f&8>_1IkXqpWZxTBJ#a z9aT#CNu`+@H>(|XRr#d1v#vItFRLk@Z6y+Ih8h$uBE3)>x*47l#MQ&0i}9lkh&v1I zZ^PY-dQ_P@^Rt3nsrN^`&X!ZvysBR3G83CG`^>1IY}zTNdKJdC*kW3(Fw3DT4i4uW zlRQa9YSE$-tDUKlsp4pKvASi&Z z?x}E#A(!9O2E(m1JF>|g^C=7UX>l;QUTT~1>dcAjN@g*m^y7wBNaZ-K2&vLWPp~_g z>{l*xc|Bk9*=@4eoTW$Qd~ZIjwdRLSA;62RU<{-G{x9Ip=IOW=X4ZrDet0&B0VM%i zidl(_OVTdYmWT1>6vg?i(-?_8I1I&;XYA2b!C7^xYX zF~aGv9Iq#n@@h2hj;7>NqQ-`j^U6vvq+mMq?T5{b>jQS$Z)A(R-fTRVOLbB>%iLg; z5i_++nmMJLN~sgm$Vs)d8x^}+t5DxpJ8iYn=x?Ii{zKyE3_7Ak!f)wzKl3SemV8TYWP=$ijZbh*WkdaV>g26%*=CNK))3mYpZ1rGBiJ53$X4 zcueg0%v3&J4y%6ZxZwBQiZ-X_{fqiJX;ce5dPIEEWOlEu6W0IC%~UX#MG6^rStRZDtRClCoi%A9 z{^x(4AS$LeSYlp+{_&H5qINN52?Ftudm~*C_ADKq09Y=7eo=zqRO33Ut^{3ki^Wv~ z&13We4s6IY14Hupmf;uAHmoYH&&W8~k7Oe}xgUW>cyQnEU=??xzKsqDL~ivy3@rx+ zMEWg{i*RHQFdD4T9_AQQw^ zJ971s%QwzoLo$NK{Uo4EtDPPHf%uMkF$90MS$Jo>Tk8PYvD{HM_i__{c?xI=UlOmf&R*gh z?Si@cb%W8~u@>jPBtg;*`-gu9W#-I90r`Q{fcwqzpG_d#e*bj$|m1)lULPvigRF9z8aOUM;;A z`!8)}kQ5wjGQoMPkZXCmbG}dp;^tH`StHi^P_zQaDVuq~_rW zsA!BjQSeBLBCn!L&Q>i7ww<`wav4F2iq2jy3ciSIt*cTLOgKAwR9v=dQLw@M6oh!FC?GG=rR9nZ!cvjXZrAK*yTj1Si}T;mRPxFW6PYru3izoT`&HY@kk^tIb|30N zujSsz{4b*2Y!xc)Bg-g6r#vnY1ZCK217#&?+zoHgy+dR+{gJmK_#LW&!@dE5P$sOZ zvRBx>q)E6F2r2=9VL!Ndau77@t&t9TEV6+azD!3|gfD%hH(Mrk$VkZt$VnZ{rjYao zbx`+?XbUST8_7R_x;$mOG>4ECBn1bJF%&4k2=v)3Tr0{}GfG_}(ks*C%dea-XlzHH zm1c_$uHfzYmM;tp@>QX!nfmfn@(85>o&_=w&Uz2eaX=|lyZd*EXCpUmxjzZ$mm8rW z(>NTyfaD^)S~)GRoP*JS+y$!H>^R9Aq}>efzhlh5gv0cG^T^JuHR%5;b|0C1 z4{zfJ4p{*jG7i2F{_gdENv~VL(5~|Phlpe4b2;UM)hqLsjmYKG4b|VUW&zVNNn9kB z+MC9)GB2V;2Em5{dnKLKe;~sI<2~K(%Zkoa$y;1Ht3$KOtnHj)`+jC66zeJYW^zkC zKH?g*z}z>`Iwo&$jI1FO=uh0Hwv7wyLd0Is0T(FW`ngQh$`1AFV|6dlYdZCx4_1Bv zucVSyH>hG1apaQL&Bs@+8R<%9e0Mws7Uj6bF z+?NOCXH(t8ciM@2tUl1le4+X!G;$qWYt@9vDYAz1(5K%>GQy(mYy;lBNYd?=U|-Vh z;24>0k=0pQ(^+ntSc_l}0T;_c>MiF5fzSTRQHQB10NAtbD=d#&MqdcxJC7A^TH#$1 zAi0Gw!Y!e_Jo%P=%RO|6a2SJqb@z9=d;{XsrtzKK@fsQIr5%IF$AaBY`j%+C`VgHK z9*|17?y-wK`(>&5xW1sH1nG4sa!Z95PP(Fi^iem1_AeOHFVBu7#J^-h*!eKK{@Xe6 zRZpOlSGX+a;`fUKAk1wZBlPZlH`C!HYIp+xhaV(Z&Ar?hv0cIPV4KK5ILO@|mwYNb zT&2^7y4p>l>7EUH&E|LWY;)*%CrRLx_VVnA(DCdHjQjHPRr{ka+R5D5!I^dgTgXNY%yUG8(XGDK1Ku_Y{A!*+&HV=F@jVRBKIC2ya75qp|Jw)rZqz| z7osVPK+ql{(O5l<43zMofRKQRztN1X03ibgl@^4>XSx!;q_X5;Z8Y7jJuP8%H+R7y5)ltrbL+Dy&_kM$oAWt-S`3=~|(4_q0 zm`@^C}dH<&V82Purv9buG zXuzQbZ?MT^GLjIf%SG3*r1cHdLms2)5cA21RBJ!l1)bQ8+~{cf z!POjG6mhm#Ll`$sj8K}EurxEBq`l0#O&W#Jv>t8@d4G}aG$eyGsno-67&(^N-9b zQnuTUhXH*kpfGJ+f`II=FL$8=TXMXp+y@J6%Y0u50dl_`py$2^gDs!$ff*Ld_u%ey zv*YRF9vFO}xCay{PRp{^AI4_g>JMa^ruu`}o+j=A1^0{j$Y6muXP+7W_MzFAwi&tk z&b~W9VcNO`0o~5N0I0y0JZ4`ISYTVO*%t_Mza6WW{b3xl&mYV=`~2bVbhF3o^9Qrd zK7XJ{XSS=Tdqm!1#&Y<{egnJ&oxQ$4Mfm5R$_T@dLgaN9GD@XsaDS|U(A(%5~ zk^vk{12$s`7iPH*=ttM-`^j_!eaC*FCTZ{`<=yikCE)6HbELBZOM?g>8UL#GY~pA* zK;E2J2}n=C-R0&6YD9xUE%6Gsg(>iCXI)VP;BXE&ZU|UXQ11|MUA3wkFHxwjD1(56 zaZZtSR+QnRUNVSslRD$_gPKA1vN%(KMcG-CU|$3`=%&FoWX#AtkUz8Ojd|-Kts*Nu zJudP~{O(1@O^6`Boa|neamXn&oSQKt3HW)!*dzTjw`z?ROq89LxI_JzY9I;xSxb)&$(a```7~~NKOG6bA9$I)1=0H zFmy!!M3ciIm7CnQk4wZSnXIR}8LmHQxaMu^mNUp29#{5cABOADZ30+h#(HN~4ZA_3 zFODS*DWRN(6(S%6<@9{=&7F!V3N{3Jggh^U4>N}hJ|mM)Uin8IBza^3jb^nHbhdlQ z)Zg*0Vx-!1(Cm}YGK^=-BhS+O>oy`0BtA` zg?FCE2y31rEh8q)D;J{iDag_&(!I`=;t#j)nP|`c&6)1b_~M&%&PVHq?kP5-5ql0e zqfu8THm|1edxGno!LG6fqa@53$Yc22WK+Y0dF3rLKY3&i^E6-qhFzdLQr2GV6I&lJ zmqVAopj=;`Mm<4lM&FRg#xo4GKMoF@Fg z_xDhcDE^}B*iD9|uW{)#FhV>8T~5ApjQVCTdVHvQ)#lOpzp+CH_c$KPk7rqo%X!AIUG{ax?cY`E_W9Xp5X6Yr@l0McH3fkTZ&S=3WRg zNoI+@swm4?T@W?>>8UTMJ5dAn0feX#Ye5wjS7N2IvM%ti1h`5Q%97yLAgRVHp)0^7 zngE&~MJAL5csLd16~PjmvcEnlOG00Z8lfpm{miNY!OA?rYMR1{tS;~buW-kmAnVZB zA6TOZ8u6^J1>(;UuJPv!M8XTK^d!mz_-1?}&Z53njyh1V=&HzpHeLx)=AGIXMxvr{z(VMQts>I=n(?3d9H=k3b0k zUn-KMoB?Z`BJ)t$2hu7rYk^=F%1MB{puPp+jW{H3*a3U1SMeup%}Nrn5U{`mo)Be% zwXvfD^qLNyI9Z7R-V6Yro6WbUC#FFWqh5`fmWC8TkBI)TH=(OkmH4wXBIwbduf&vQ z8oeN2G=-KLrO>r;(px}eQkZ%CAgerd?im2tZ? zB>LlFzc-?YQnyNUdtJI)8`9l6)uFn~8$ox8ZjYcQRF@$}jZ(V}P(78#Ortjh^bwU_ ze?Fw^4Tfm++Er>eBFYrerc32Eg-~_pM5SG#JFi5w)G5^|SgXf0sNoX;L(~z|1_g=$ zzEYPcRT#S01#DD$U1nISFs}sD8!~3qDLtZIiPDfB0fE$pz0T_s5NEFjKE C`vkrK diff --git a/packages/cli-old/src/cli/add/auth.ts b/packages/cli-old/src/cli/add/auth.ts deleted file mode 100644 index cab277f9..00000000 --- a/packages/cli-old/src/cli/add/auth.ts +++ /dev/null @@ -1,110 +0,0 @@ -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 deleted file mode 100644 index 7d460058..00000000 --- a/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts +++ /dev/null @@ -1,96 +0,0 @@ -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.proof.sh/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 deleted file mode 100644 index e7c00218..00000000 --- a/packages/cli-old/src/cli/add/data-source/filemaker.ts +++ /dev/null @@ -1,441 +0,0 @@ -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, ensureWebviewerFmMcpConfig } from "~/generators/fmdapi.js"; -import { getFmMcpStatus } from "~/helpers/fmMcp.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 fmMcpStatus = await getFmMcpStatus(); - const connectedFileName = fmMcpStatus.connectedFiles[0]; - const localDataSourceName = opts.name ?? "filemaker"; - - if (!opts.server && fmMcpStatus.healthy && connectedFileName) { - addPackageDependency({ - projectDir, - dependencies: ["@proofkit/fmdapi"], - devMode: false, - }); - - await ensureWebviewerFmMcpConfig({ - projectDir, - connectedFileName, - dataSourceName: localDataSourceName, - baseUrl: fmMcpStatus.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 MCP at ${fmMcpStatus.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 MCP connection was detected and no FileMaker server was provided. Start the local FM MCP proxy with a connected file or rerun with --server.", - ); - } - - if (!opts.server) { - const fallbackAction = abortIfCancel( - await p.select({ - message: - "Local FM MCP 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 MCP 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 deleted file mode 100644 index 6d73789b..00000000 --- a/packages/cli-old/src/cli/add/data-source/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index b63dc427..00000000 --- a/packages/cli-old/src/cli/add/fmschema.ts +++ /dev/null @@ -1,216 +0,0 @@ -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 deleted file mode 100644 index b3e0d543..00000000 --- a/packages/cli-old/src/cli/add/index.ts +++ /dev/null @@ -1,190 +0,0 @@ -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 deleted file mode 100644 index 8ed95b1d..00000000 --- a/packages/cli-old/src/cli/add/page/index.ts +++ /dev/null @@ -1,230 +0,0 @@ -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 deleted file mode 100644 index fcccbb27..00000000 --- a/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index a6e930ed..00000000 --- a/packages/cli-old/src/cli/add/page/post-install/table.ts +++ /dev/null @@ -1,123 +0,0 @@ -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 deleted file mode 100644 index a49d5740..00000000 --- a/packages/cli-old/src/cli/add/page/templates.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index 7b7da162..00000000 --- a/packages/cli-old/src/cli/add/page/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 9e778b10..00000000 --- a/packages/cli-old/src/cli/add/registry/getOptions.ts +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 5625d73b..00000000 --- a/packages/cli-old/src/cli/add/registry/http.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 368a84a0..00000000 --- a/packages/cli-old/src/cli/add/registry/install.ts +++ /dev/null @@ -1,224 +0,0 @@ -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 deleted file mode 100644 index 046f5c73..00000000 --- a/packages/cli-old/src/cli/add/registry/listItems.ts +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 2ef0b79c..00000000 --- a/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts +++ /dev/null @@ -1,189 +0,0 @@ -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 deleted file mode 100644 index 6afb4233..00000000 --- a/packages/cli-old/src/cli/add/registry/postInstall/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 50df220c..00000000 --- a/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index dd1ec34e..00000000 --- a/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts +++ /dev/null @@ -1,132 +0,0 @@ -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 deleted file mode 100644 index a7e973f0..00000000 --- a/packages/cli-old/src/cli/add/registry/preflight.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index ecc1deac..00000000 --- a/packages/cli-old/src/cli/deploy/index.ts +++ /dev/null @@ -1,489 +0,0 @@ -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 deleted file mode 100644 index cb252b8a..00000000 --- a/packages/cli-old/src/cli/fmdapi.ts +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 4b8cc21c..00000000 --- a/packages/cli-old/src/cli/init.ts +++ /dev/null @@ -1,395 +0,0 @@ -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 deleted file mode 100644 index be40d6a7..00000000 --- a/packages/cli-old/src/cli/menu.ts +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index 569ad348..00000000 --- a/packages/cli-old/src/cli/ottofms.ts +++ /dev/null @@ -1,268 +0,0 @@ -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 deleted file mode 100644 index c4b7900e..00000000 --- a/packages/cli-old/src/cli/prompts.ts +++ /dev/null @@ -1,188 +0,0 @@ -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 deleted file mode 100644 index f0ac245a..00000000 --- a/packages/cli-old/src/cli/react-email.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index dbc6aebf..00000000 --- a/packages/cli-old/src/cli/remove/data-source.ts +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index 954e8765..00000000 --- a/packages/cli-old/src/cli/remove/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index d6574589..00000000 --- a/packages/cli-old/src/cli/remove/page.ts +++ /dev/null @@ -1,214 +0,0 @@ -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 deleted file mode 100644 index 4cc40088..00000000 --- a/packages/cli-old/src/cli/remove/schema.ts +++ /dev/null @@ -1,100 +0,0 @@ -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 deleted file mode 100644 index fb29fac0..00000000 --- a/packages/cli-old/src/cli/tanstack-query.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 23a4f61c..00000000 --- a/packages/cli-old/src/cli/typegen/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 93eca92b..00000000 --- a/packages/cli-old/src/cli/update/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 8232b3fe..00000000 --- a/packages/cli-old/src/cli/update/makeUpgradeCommand.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 37a6897f..00000000 --- a/packages/cli-old/src/cli/utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 45a00383..00000000 --- a/packages/cli-old/src/consts.ts +++ /dev/null @@ -1,35 +0,0 @@ -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.proof.sh"; - -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.proof.sh"; diff --git a/packages/cli-old/src/generators/auth.ts b/packages/cli-old/src/generators/auth.ts deleted file mode 100644 index 3ecd4080..00000000 --- a/packages/cli-old/src/generators/auth.ts +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 27e54ab0..00000000 --- a/packages/cli-old/src/generators/fmdapi.ts +++ /dev/null @@ -1,525 +0,0 @@ -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.proof.sh/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.proof.sh/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.proof.sh/typegen-config-schema.json", - config: configsToAdd, - }; - } - - await writeJsonConfigFile(jsonConfigPath, fileContent); - - if (runCodegen) { - await runCodegenCommand(); - } -} - -export async function ensureWebviewerFmMcpConfig({ - 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: [], - fmMcp: { - enabled: true, - ...(baseUrl ? { baseUrl } : {}), - ...(connectedFileName ? { connectedFileName } : {}), - }, - }; - - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - fileContent = { - $schema: "https://proofkit.proof.sh/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 ?? [], - fmMcp: { - enabled: true, - ...(existingConfig.fmMcp ?? {}), - ...(newConfig.fmMcp ?? {}), - }, - }; - } - - 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.proof.sh/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 deleted file mode 100644 index e008a05c..00000000 --- a/packages/cli-old/src/generators/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -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., Web Viewer 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 deleted file mode 100644 index 874eee0d..00000000 --- a/packages/cli-old/src/generators/tanstack-query.ts +++ /dev/null @@ -1,97 +0,0 @@ -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 deleted file mode 100644 index 5fbdeef7..00000000 --- a/packages/cli-old/src/globalOptions.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index edd6438c..00000000 --- a/packages/cli-old/src/globals.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index cb354d95..00000000 --- a/packages/cli-old/src/helpers/createProject.ts +++ /dev/null @@ -1,129 +0,0 @@ -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/fmMcp.ts b/packages/cli-old/src/helpers/fmMcp.ts deleted file mode 100644 index ab58114e..00000000 --- a/packages/cli-old/src/helpers/fmMcp.ts +++ /dev/null @@ -1,56 +0,0 @@ -const defaultBaseUrl = process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365"; -const REQUEST_TIMEOUT_MS = 3000; - -export interface FmMcpStatus { - 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 getFmMcpStatus(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 getFmMcpStatus(baseUrl); - return status.connectedFiles[0]; -} diff --git a/packages/cli-old/src/helpers/git.ts b/packages/cli-old/src/helpers/git.ts deleted file mode 100644 index bdeaefee..00000000 --- a/packages/cli-old/src/helpers/git.ts +++ /dev/null @@ -1,140 +0,0 @@ -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 deleted file mode 100644 index 880bd436..00000000 --- a/packages/cli-old/src/helpers/installDependencies.ts +++ /dev/null @@ -1,242 +0,0 @@ -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 deleted file mode 100644 index 06345c47..00000000 --- a/packages/cli-old/src/helpers/installPackages.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 5b7c845d..00000000 --- a/packages/cli-old/src/helpers/logNextSteps.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index e7f9d4b1..00000000 --- a/packages/cli-old/src/helpers/replaceText.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 7905eb0a..00000000 --- a/packages/cli-old/src/helpers/scaffoldProject.ts +++ /dev/null @@ -1,136 +0,0 @@ -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 deleted file mode 100644 index 4b538d3d..00000000 --- a/packages/cli-old/src/helpers/selectBoilerplate.ts +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 7551134b..00000000 --- a/packages/cli-old/src/helpers/setImportAlias.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 4c235380..00000000 --- a/packages/cli-old/src/helpers/shadcn-cli.ts +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 6f865ab8..00000000 --- a/packages/cli-old/src/helpers/stealth-init.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 26a21e80..00000000 --- a/packages/cli-old/src/helpers/version-fetcher.ts +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index a61c41c7..00000000 --- a/packages/cli-old/src/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -#!/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 deleted file mode 100644 index 20f1401d..00000000 --- a/packages/cli-old/src/installers/auth-shared.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index f417ab36..00000000 --- a/packages/cli-old/src/installers/better-auth.ts +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 11ddd816..00000000 --- a/packages/cli-old/src/installers/clerk.ts +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index c5b53268..00000000 --- a/packages/cli-old/src/installers/dependencyVersionMap.ts +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index 2eb95da9..00000000 --- a/packages/cli-old/src/installers/envVars.ts +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index b4fb6fb6..00000000 --- a/packages/cli-old/src/installers/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index b799a68e..00000000 --- a/packages/cli-old/src/installers/install-fm-addon.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 Web Viewer"; - - 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.proof.sh/auth/fm-addon)")}`, - ); - } else { - console.log( - `${chalk.yellowBright( - "You must install the ProofKit Web Viewer addon in your FileMaker file to continue.", - )} ${chalk.dim("(Learn more: https://proofkit.proof.sh/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 deleted file mode 100644 index 163df0f9..00000000 --- a/packages/cli-old/src/installers/nextAuth.ts +++ /dev/null @@ -1,189 +0,0 @@ -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.proof.sh/auth/next-auth\n`, - ); -} diff --git a/packages/cli-old/src/installers/proofkit-auth.ts b/packages/cli-old/src/installers/proofkit-auth.ts deleted file mode 100644 index 89b80532..00000000 --- a/packages/cli-old/src/installers/proofkit-auth.ts +++ /dev/null @@ -1,220 +0,0 @@ -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 deleted file mode 100644 index 0635de12..00000000 --- a/packages/cli-old/src/installers/proofkit-webviewer.ts +++ /dev/null @@ -1,84 +0,0 @@ -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 Web Viewer 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 deleted file mode 100644 index 59a90749..00000000 --- a/packages/cli-old/src/installers/react-email.ts +++ /dev/null @@ -1,211 +0,0 @@ -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 deleted file mode 100644 index 58711d4b..00000000 --- a/packages/cli-old/src/state.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 1338225f..00000000 --- a/packages/cli-old/src/upgrades/cursorRules.ts +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index b72dbc98..00000000 --- a/packages/cli-old/src/upgrades/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index d385a4d0..00000000 --- a/packages/cli-old/src/upgrades/shadcn.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index c2d139e7..00000000 --- a/packages/cli-old/src/utils/addPackageDependency.ts +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 5af7e131..00000000 --- a/packages/cli-old/src/utils/addToEnvs.ts +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index 8522e45a..00000000 --- a/packages/cli-old/src/utils/formatting.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 496e46a2..00000000 --- a/packages/cli-old/src/utils/getProofKitVersion.ts +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index d2e3afdc..00000000 --- a/packages/cli-old/src/utils/getUserPkgManager.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index ccf602ed..00000000 --- a/packages/cli-old/src/utils/isTTYError.ts +++ /dev/null @@ -1 +0,0 @@ -export class IsTTYError extends Error {} diff --git a/packages/cli-old/src/utils/logger.ts b/packages/cli-old/src/utils/logger.ts deleted file mode 100644 index 3ddb9775..00000000 --- a/packages/cli-old/src/utils/logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index a4d4507e..00000000 --- a/packages/cli-old/src/utils/parseNameAndPath.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index eb77a8ec..00000000 --- a/packages/cli-old/src/utils/parseSettings.ts +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index aa7ecf17..00000000 --- a/packages/cli-old/src/utils/proofkitReleaseChannel.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 deleted file mode 100644 index 051c3322..00000000 --- a/packages/cli-old/src/utils/removeTrailingSlash.ts +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index d0f89738..00000000 --- a/packages/cli-old/src/utils/renderTitle.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index fd046831..00000000 --- a/packages/cli-old/src/utils/renderVersionWarning.ts +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 92d58954..00000000 --- a/packages/cli-old/src/utils/ts-morph.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index b5b4e42e..00000000 --- a/packages/cli-old/src/utils/validateAppName.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index bd33ca61..00000000 --- a/packages/cli-old/src/utils/validateImportAlias.ts +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 5ce7a9e0..00000000 --- a/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc +++ /dev/null @@ -1,51 +0,0 @@ ---- -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 deleted file mode 100644 index 3b030fa5..00000000 --- a/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc +++ /dev/null @@ -1,60 +0,0 @@ ---- -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 deleted file mode 100644 index d25da047..00000000 --- a/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc +++ /dev/null @@ -1,65 +0,0 @@ ---- -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 deleted file mode 100644 index 5672e80e..00000000 --- a/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc +++ /dev/null @@ -1,60 +0,0 @@ ---- -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 deleted file mode 100644 index 061da499..00000000 --- a/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc +++ /dev/null @@ -1,88 +0,0 @@ ---- -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 deleted file mode 100644 index dd6d6716..00000000 --- a/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc +++ /dev/null @@ -1,176 +0,0 @@ ---- -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 deleted file mode 100644 index 797fd3cc..00000000 --- a/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc +++ /dev/null @@ -1,240 +0,0 @@ ---- -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 deleted file mode 100644 index 78ec63ad..00000000 --- a/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc +++ /dev/null @@ -1,57 +0,0 @@ ---- -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 deleted file mode 100644 index 1f71d754..00000000 --- a/packages/cli-old/template/extras/config/drizzle-config-mysql.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index d2a21ed7..00000000 --- a/packages/cli-old/template/extras/config/drizzle-config-postgres.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 34f8fa24..00000000 --- a/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 660edd23..00000000 --- a/packages/cli-old/template/extras/config/fmschema.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @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 deleted file mode 100644 index 44598cba..00000000 --- a/packages/cli-old/template/extras/config/get-query-client.ts +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 4cdb2f43..00000000 --- a/packages/cli-old/template/extras/config/postcss.config.cjs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 5af4ad27..00000000 --- a/packages/cli-old/template/extras/config/query-provider-vite.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 2afa87bd..00000000 --- a/packages/cli-old/template/extras/config/query-provider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"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 deleted file mode 100644 index b8ed06e6..00000000 --- a/packages/cli-old/template/extras/emailProviders/none/email.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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.proof.sh/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 deleted file mode 100644 index ef94053e..00000000 --- a/packages/cli-old/template/extras/emailProviders/plunk/email.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 9f6a3ca6..00000000 --- a/packages/cli-old/template/extras/emailProviders/plunk/service.ts +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 5ca905b8..00000000 --- a/packages/cli-old/template/extras/emailProviders/resend/email.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 9af08cd1..00000000 --- a/packages/cli-old/template/extras/emailProviders/resend/service.ts +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 3661a457..00000000 --- a/packages/cli-old/template/extras/emailTemplates/auth-code.tsx +++ /dev/null @@ -1,137 +0,0 @@ -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) => ( - - - - - ProofKit - {type === "verification" ? "Verify Your Email" : "Reset Your Password"} - - Enter the following code to {type === "verification" ? "verify your email" : "reset your password"} - -

- {validationCode} -
- 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 deleted file mode 100644 index f4b34ba2..00000000 --- a/packages/cli-old/template/extras/emailTemplates/generic.tsx +++ /dev/null @@ -1,113 +0,0 @@ -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) => ( - - - - - ProofKit - - {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.proof.sh", - 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 deleted file mode 100644 index 49191dfa..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts +++ /dev/null @@ -1,97 +0,0 @@ -"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 deleted file mode 100644 index 76431716..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 13e3853a..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"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 ( -
- - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - {form.formState.isDirty && ( - - - - )} - -
- ); -} 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 deleted file mode 100644 index b22bee20..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"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 ( -
- - - - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - - - -
- ); -} 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 deleted file mode 100644 index 046783e4..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 78c14d96..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts +++ /dev/null @@ -1,39 +0,0 @@ -"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 deleted file mode 100644 index 695d7f2c..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"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 ( -
- - - - - {action.result.data?.error && ( - {action.result.data.error} - )} - - - - -
- ); -} 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 deleted file mode 100644 index 09be86ba..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 15829b1a..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index ca66a9df..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts +++ /dev/null @@ -1,35 +0,0 @@ -"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 deleted file mode 100644 index c9967827..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"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 ( -
- - - - - - - - Forgot password? - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} 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 deleted file mode 100644 index a98eb6c3..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 66276d2f..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index a781546c..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts +++ /dev/null @@ -1,53 +0,0 @@ -"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 deleted file mode 100644 index 9a164a1d..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index e11b3acd..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"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 ( -
- - - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - - -
- ); -} 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 deleted file mode 100644 index 8315fd2c..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 4ce0b1b7..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts +++ /dev/null @@ -1,46 +0,0 @@ -"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 deleted file mode 100644 index b3796b06..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 37d5311a..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 2d454b7e..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"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 ( -
- - - { - form.setValue("code", value); - if (value.length === 8) { - handleSubmitWithAction(); - } - }} - error={!!form.formState.errors.code?.message} - autoFocus - /> - {form.formState.errors.code?.message && ( - {form.formState.errors.code.message} - )} - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} 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 deleted file mode 100644 index 3faa5d0f..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts +++ /dev/null @@ -1,50 +0,0 @@ -"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 deleted file mode 100644 index 056d5284..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index e15638ca..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 75c0e4fd..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"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 ( -
- - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} 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 deleted file mode 100644 index 3ad9697a..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts +++ /dev/null @@ -1,109 +0,0 @@ -"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 deleted file mode 100644 index 3108c3fa..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"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 ( -
- - - { - form.setValue("code", value); - if (value.length === 8) { - handleSubmitWithAction(); - } - }} - /> - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} 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 deleted file mode 100644 index bfad170d..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index ee36ae70..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"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 deleted file mode 100644 index d962f424..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index c4e4c11f..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts +++ /dev/null @@ -1,19 +0,0 @@ -"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 deleted file mode 100644 index 9bce1e21..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 40a2afef..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"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 deleted file mode 100644 index 46bec5b2..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index e4fd0778..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"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 ( - - - - - - } - > - My Profile - - - } - onClick={logout} - > - Sign out - - - - ); -} 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 deleted file mode 100644 index 3661a457..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx +++ /dev/null @@ -1,137 +0,0 @@ -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) => ( - - - - - ProofKit - {type === "verification" ? "Verify Your Email" : "Reset Your Password"} - - Enter the following code to {type === "verification" ? "verify your email" : "reset your password"} - -
- {validationCode} -
- 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 deleted file mode 100644 index 86ea06f7..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/middleware.ts +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 253eab74..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts +++ /dev/null @@ -1,137 +0,0 @@ -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 deleted file mode 100644 index 377f773d..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index 41849aef..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index d3434460..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index bf723a6f..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index eb3b467b..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index aaa80f35..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts +++ /dev/null @@ -1,191 +0,0 @@ -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 deleted file mode 100644 index 1b7e0194..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts +++ /dev/null @@ -1,146 +0,0 @@ -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 deleted file mode 100644 index 6b9dd139..00000000 --- a/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index ddb6e099..00000000 --- a/packages/cli-old/template/extras/prisma/schema/base.prisma +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index 198915b9..00000000 --- a/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index b17831e6..00000000 --- a/packages/cli-old/template/extras/prisma/schema/with-auth.prisma +++ /dev/null @@ -1,74 +0,0 @@ -// 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 deleted file mode 100644 index ebe15eab..00000000 --- a/packages/cli-old/template/extras/src/app/_components/post-tw.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"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.

- )} -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className="flex flex-col gap-2" - > - setName(e.target.value)} - className="w-full rounded-full px-4 py-2 text-black" - /> - -
-
- ); -} diff --git a/packages/cli-old/template/extras/src/app/_components/post.tsx b/packages/cli-old/template/extras/src/app/_components/post.tsx deleted file mode 100644 index 1ad81347..00000000 --- a/packages/cli-old/template/extras/src/app/_components/post.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"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.

- )} - -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className={styles.form} - > - setName(e.target.value)} - className={styles.input} - /> - -
-
- ); -} 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 deleted file mode 100644 index fbb80152..00000000 --- a/packages/cli-old/template/extras/src/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 5fbd827d..00000000 --- a/packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 4382acb8..00000000 --- a/packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 2cc13d4c..00000000 --- a/packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 27439454..00000000 --- a/packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index e0382db7..00000000 --- a/packages/cli-old/template/extras/src/app/layout/base.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 77fa7adf..00000000 --- a/packages/cli-old/template/extras/src/app/layout/main-shell.tsx +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index c1218810..00000000 --- a/packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 6471a2ae..00000000 --- a/packages/cli-old/template/extras/src/app/layout/with-trpc.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 5dea6caf..00000000 --- a/packages/cli-old/template/extras/src/app/layout/with-tw.tsx +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 51933f24..00000000 --- a/packages/cli-old/template/extras/src/app/next-auth/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index b4781c4d..00000000 --- a/packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -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 ( - -
{ - "use server"; - try { - await signIn("credentials", formData); - } catch (error) { - if (error instanceof AuthError) { - return redirect(`/auth/signin?error=${error.type}`); - } - throw error; - } - }} - > - - - - - - -
- {providerMap.length > 0 && ( - <> - - {Object.values(providerMap).map((provider) => ( -
{ - "use server"; - try { - await signIn(provider.id, { - redirectTo: searchParams.callbackUrl ?? "", - }); - } catch (error) { - // Signin can fail for a number of reasons, such as the user - // not existing, or the user not having the correct role. - // In some cases, you may want to redirect to a custom error - if (error instanceof AuthError) { - return redirect(`/auth/signin?error=${error.type}`); - } - - // Otherwise if a redirects happens Next.js can handle it - // so you can just re-thrown the error and let Next.js handle it. - // Docs: - // https://nextjs.org/docs/app/api-reference/functions/redirect#server-component - throw error; - } - }} - > - -
- ))} - - )} - - - {"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 deleted file mode 100644 index fba6508d..00000000 --- a/packages/cli-old/template/extras/src/app/next-auth/signup/action.ts +++ /dev/null @@ -1,24 +0,0 @@ -"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 deleted file mode 100644 index faab245f..00000000 --- a/packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"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 deleted file mode 100644 index d3086d30..00000000 --- a/packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index bf905890..00000000 --- a/packages/cli-old/template/extras/src/app/page/base.tsx +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 49c9bbbe..00000000 --- a/packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index cfeed2f5..00000000 --- a/packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index d7121d83..00000000 --- a/packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 035f250b..00000000 --- a/packages/cli-old/template/extras/src/app/page/with-trpc.tsx +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 773fef1b..00000000 --- a/packages/cli-old/template/extras/src/app/page/with-tw.tsx +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 50e2f512..00000000 --- a/packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"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 deleted file mode 100644 index 33683736..00000000 --- a/packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"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 deleted file mode 100644 index 6f8da571..00000000 --- a/packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"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 deleted file mode 100644 index e6f328f4..00000000 --- a/packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx +++ /dev/null @@ -1,14 +0,0 @@ -"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 deleted file mode 100644 index 5cadae53..00000000 --- a/packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"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 deleted file mode 100644 index a1305c5d..00000000 --- a/packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"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 ( - - - - - - signOut()}>Sign Out - - - ); - - 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 deleted file mode 100644 index e73bf132..00000000 --- a/packages/cli-old/template/extras/src/env/with-auth.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index e9825af7..00000000 --- a/packages/cli-old/template/extras/src/env/with-clerk.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index fac9982a..00000000 --- a/packages/cli-old/template/extras/src/index.module.css +++ /dev/null @@ -1,177 +0,0 @@ -.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 deleted file mode 100644 index 1dd75bb4..00000000 --- a/packages/cli-old/template/extras/src/middleware/clerk.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index e1f450d4..00000000 --- a/packages/cli-old/template/extras/src/middleware/next-auth.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index e7e7fb29..00000000 --- a/packages/cli-old/template/extras/src/pages/_app/base.tsx +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 89d10b0c..00000000 --- a/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 89d10b0c..00000000 --- a/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index a008ed16..00000000 --- a/packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index a008ed16..00000000 --- a/packages/cli-old/template/extras/src/pages/_app/with-auth.tsx +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 464c50cc..00000000 --- a/packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 464c50cc..00000000 --- a/packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index da39269a..00000000 --- a/packages/cli-old/template/extras/src/pages/_app/with-tw.tsx +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 8739530f..00000000 --- a/packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 587dd2bd..00000000 --- a/packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index a34888c6..00000000 --- a/packages/cli-old/template/extras/src/pages/index/base.tsx +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 532e7f73..00000000 --- a/packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index f3191246..00000000 --- a/packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index 3a51c3e8..00000000 --- a/packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 26d807f9..00000000 --- a/packages/cli-old/template/extras/src/pages/index/with-trpc.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 88b818e2..00000000 --- a/packages/cli-old/template/extras/src/pages/index/with-tw.tsx +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index b341fc4d..00000000 --- a/packages/cli-old/template/extras/src/server/api/root.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 6781c531..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/base.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 35ac7ba8..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index f0140b76..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 8ea389bf..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index a295842c..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 3282fa6e..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index e831d1a8..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * 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 deleted file mode 100644 index a8ca5724..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * 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 deleted file mode 100644 index 55e20cf1..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * 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 deleted file mode 100644 index c77a7530..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * 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 deleted file mode 100644 index b1d9f34c..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * 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 deleted file mode 100644 index f057f427..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * 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 deleted file mode 100644 index 87a6b0e2..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * 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 deleted file mode 100644 index a6e5bef7..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * 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 deleted file mode 100644 index fac1dc35..00000000 --- a/packages/cli-old/template/extras/src/server/data/users.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 52188938..00000000 --- a/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 07dc0271..00000000 --- a/packages/cli-old/template/extras/src/server/db/db-prisma.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 3542b7b8..00000000 --- a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 4613a4c1..00000000 --- a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 1287189a..00000000 --- a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index ef1df14a..00000000 --- a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index bfb08079..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index bfb08079..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index 8e6f2f99..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 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 deleted file mode 100644 index cc74c86a..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 96e9a85c..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts +++ /dev/null @@ -1,123 +0,0 @@ -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 deleted file mode 100644 index a0b1d72f..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index 5ce3f9c2..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts +++ /dev/null @@ -1,130 +0,0 @@ -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 deleted file mode 100644 index 12ee2901..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts +++ /dev/null @@ -1,116 +0,0 @@ -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 deleted file mode 100644 index ac29d61f..00000000 --- a/packages/cli-old/template/extras/src/server/next-auth/base.ts +++ /dev/null @@ -1,111 +0,0 @@ - - - - -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 deleted file mode 100644 index a82f34c6..00000000 --- a/packages/cli-old/template/extras/src/server/next-auth/password.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 6e9281d1..00000000 --- a/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 117984c9..00000000 --- a/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index bda64397..00000000 --- a/packages/cli-old/template/extras/src/trpc/query-client.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 8c0521a7..00000000 --- a/packages/cli-old/template/extras/src/trpc/react.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"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 deleted file mode 100644 index 59300a63..00000000 --- a/packages/cli-old/template/extras/src/trpc/server.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 0f03d307..00000000 --- a/packages/cli-old/template/extras/src/utils/api.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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 deleted file mode 100755 index 268df5cc..00000000 --- a/packages/cli-old/template/extras/start-database/mysql.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/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 deleted file mode 100755 index 11fb2042..00000000 --- a/packages/cli-old/template/extras/start-database/postgres.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/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/nextjs-mantine/README.md b/packages/cli-old/template/nextjs-mantine/README.md deleted file mode 100644 index 15794a3d..00000000 --- a/packages/cli-old/template/nextjs-mantine/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# ProofKit NextJS Template - -This is a [NextJS](https://nextjs.org/) project bootstrapped with `@proofkit/cli`. Learn more at [proofkit.proof.sh](https://proofkit.proof.sh) - -## What's next? How do I make an app with this? - -While this template is designed to be a minimal starting point, the proofkit CLI will guide you through adding additional features and pages. - -To add new things to your project, simply run the `proofkit` script from the project's root directory. - -e.g. `npm run proofkit` or `pnpm proofkit` etc. - -For more information, see the full [ProofKit documentation](https://proofkit.proof.sh). - -## Project Structure - -ProofKit projects have an opinionated structure to help you get started and some conventions must be maintained to ensure that the CLI can properly inject new features and components. - -The `src` directory is the home for your application code. It is used for most things except for configuration and is organized as follows: - -- `app` - NextJS app router, where your pages and routes are defined -- `components` - Shared components used throughout the app -- `server` - Code that connects to backend databases and services that should not be exposed in the browser - -Anytime you see an `internal` folder, you should not modify any files inside. These files are maintained exclusively by the ProofKit CLI and changes to them may be overwritten. - -Anytime you see a componet file that begins with `slot-`, you _may_ modify the content, but do not rename, remove, or move them. These are desigend to be customized, but are still used by the CLI to inject additional content. If a slot is not needed by your app, you can have the compoment return `null` or an empty fragment: `<>` diff --git a/packages/cli-old/template/nextjs-mantine/_gitignore b/packages/cli-old/template/nextjs-mantine/_gitignore deleted file mode 100644 index 00bba9bb..00000000 --- a/packages/cli-old/template/nextjs-mantine/_gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/packages/cli-old/template/nextjs-mantine/components.json b/packages/cli-old/template/nextjs-mantine/components.json deleted file mode 100644 index 0d27c449..00000000 --- a/packages/cli-old/template/nextjs-mantine/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/config/theme/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "tw:" - }, - "aliases": { - "components": "@/components", - "utils": "@/utils/styles", - "ui": "@/components/ui", - "lib": "@/utils", - "hooks": "@/utils/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/packages/cli-old/template/nextjs-mantine/next.config.ts b/packages/cli-old/template/nextjs-mantine/next.config.ts deleted file mode 100644 index 9555317e..00000000 --- a/packages/cli-old/template/nextjs-mantine/next.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextConfig } from "next"; - -// Import env here to validate during build. -import "./src/config/env"; - -const nextConfig: NextConfig = { - experimental: { - optimizePackageImports: ["@mantine/core", "@mantine/hooks"], - }, -}; - -export default nextConfig; diff --git a/packages/cli-old/template/nextjs-mantine/package.json b/packages/cli-old/template/nextjs-mantine/package.json deleted file mode 100644 index 31fad730..00000000 --- a/packages/cli-old/template/nextjs-mantine/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "template", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "biome check", - "format": "biome format --write", - "proofkit": "proofkit", - "typegen": "proofkit typegen", - "deploy": "proofkit deploy" - }, - "dependencies": { - "@hookform/resolvers": "^5.1.1", - "@next-safe-action/adapter-react-hook-form": "^2.0.0", - "next-safe-action": "^8.0.4", - "react-hook-form": "^7.54.2", - "@tabler/icons-react": "^3.30.0", - "@mantine/core": "^7.17.0", - "@mantine/dates": "^7.17.0", - "@mantine/hooks": "^7.17.0", - "@mantine/modals": "^7.17.0", - "@mantine/notifications": "^7.17.0", - "mantine-react-table": "2.0.0-beta.9", - "@t3-oss/env-nextjs": "^0.12.0", - "dayjs": "^1.11.13", - "next": "^15.2.7", - "react": "19.0.0", - "react-dom": "19.0.0", - "zod": "^3.24.2" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "npm:types-react@19.0.12", - "@types/react-dom": "npm:types-react-dom@19.0.4", - "@biomejs/biome": "2.3.11", - "postcss": "^8.4.41", - "ultracite": "7.0.8", - "postcss-preset-mantine": "^1.17.0", - "postcss-simple-vars": "^7.0.1", - "typescript": "^5" - }, - "pnpm": { - "overrides": { - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" - } - } -} diff --git a/packages/cli-old/template/nextjs-mantine/postcss.config.cjs b/packages/cli-old/template/nextjs-mantine/postcss.config.cjs deleted file mode 100644 index 085a0ef9..00000000 --- a/packages/cli-old/template/nextjs-mantine/postcss.config.cjs +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - plugins: { - "@tailwindcss/postcss": {}, - "postcss-preset-mantine": {}, - "postcss-simple-vars": { - variables: { - "mantine-breakpoint-xs": "36em", - "mantine-breakpoint-sm": "48em", - "mantine-breakpoint-md": "62em", - "mantine-breakpoint-lg": "75em", - "mantine-breakpoint-xl": "88em", - }, - }, - }, -}; diff --git a/packages/cli-old/template/nextjs-mantine/proofkit.json b/packages/cli-old/template/nextjs-mantine/proofkit.json deleted file mode 100644 index c536f9bf..00000000 --- a/packages/cli-old/template/nextjs-mantine/proofkit.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "auth": { "type": "none" }, - "envFile": ".env", - "appType": "browser", - "ui": "mantine", - "appliedUpgrades": ["cursorRules"] -} diff --git a/packages/cli-old/template/nextjs-mantine/public/favicon.ico b/packages/cli-old/template/nextjs-mantine/public/favicon.ico deleted file mode 100644 index ba9355b8d3f888ad3a93c0f254b6776a0c2aed92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 DtPcMKfsh_-A)LFM)6iMLm_0IRoy0b^RZ#+}< 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 b29HP@D9EFJDBd}ZfIn(VT8gjb%^v(OKI6J9 diff --git a/packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx b/packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx deleted file mode 100644 index 57a8452b..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import AppShell from "@/components/AppShell/internal/AppShell"; -import React from "react"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx b/packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx deleted file mode 100644 index 3bc6752b..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { - ActionIcon, - Anchor, - AppShellFooter, - Box, - Code, - Container, - Group, - Image, - px, - Stack, - Text, - Title, -} from "@mantine/core"; -import { IconBrandGithub, IconExternalLink } from "@tabler/icons-react"; - -export default function Home() { - return ( - <> - - - ProofKit - Welcome! - - - This is the base template home page. To add more pages, components, - or other features, run the ProofKit CLI from within your project. - - __PNPM_COMMAND__ proofkit - - - To change this page, open src/app/(main)/page.tsx - - - - ProofKit Docs - - - - - - - - - - Sponsored by{" "} - - Proof - {" "} - and{" "} - - Ottomatic - - - - -
- - - - - - - - - - ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/app/layout.tsx b/packages/cli-old/template/nextjs-mantine/src/app/layout.tsx deleted file mode 100644 index 9512cb63..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/app/layout.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Suspense } from "react"; -import { theme } from "@/config/theme/mantine-theme"; -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 "@/config/theme/globals.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/nextjs-mantine/src/app/navigation.tsx b/packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx deleted file mode 100644 index 887073db..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { type ProofKitRoute } from "@proofkit/cli"; - -export const primaryRoutes: ProofKitRoute[] = [ - { - label: "Dashboard", - type: "link", - href: "/", - exactMatch: true, - }, -]; - -export const secondaryRoutes: ProofKitRoute[] = []; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx deleted file mode 100644 index f5ea4966..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { IconInfinity } from "@tabler/icons-react"; -import React from "react"; - -export default function AppLogo() { - return ; -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx deleted file mode 100644 index 8c4270df..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Header } from "@/components/AppShell/internal/Header"; -import { AppShell, AppShellHeader, AppShellMain } from "@mantine/core"; -import React from "react"; - -import { headerHeight } from "./config"; - -export default function MainAppShell({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - -
- - - {children} - - ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css deleted file mode 100644 index 79d81bad..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.header { - /* height: rem(56px); */ - margin-bottom: rem(120px); - background-color: var(--mantine-color-body); - border-bottom: rem(1px) solid - light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); -} - -.inner { - /* height: rem(56px); */ - display: flex; - justify-content: space-between; - align-items: center; -} - -.link { - display: block; - line-height: 1; - padding: rem(8px) rem(12px); - border-radius: var(--mantine-radius-sm); - text-decoration: none; - color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); - font-size: var(--mantine-font-size-sm); - font-weight: 500; - cursor: pointer; - background: none; - border: none; - - @mixin hover { - background-color: light-dark( - var(--mantine-color-gray-0), - var(--mantine-color-dark-6) - ); - } - - [data-mantine-color-scheme] &[data-active] { - background-color: var(--mantine-primary-color-filled); - color: var(--mantine-color-white); - } -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx deleted file mode 100644 index 4409b1d6..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Box, Container, Group } from "@mantine/core"; - -import SlotHeaderCenter from "../slot-header-center"; -import SlotHeaderLeft from "../slot-header-left"; -import SlotHeaderRight from "../slot-header-right"; -import { headerHeight } from "./config"; -import classes from "./Header.module.css"; -import HeaderMobileMenu from "./HeaderMobileMenu"; - -export function Header() { - return ( -
- - - - - - - - - - - - - - -
- ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx deleted file mode 100644 index 910104fb..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { Burger, Menu } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; - -import SlotHeaderMobileMenuContent from "../slot-header-mobile-content"; - -export default function HeaderMobileMenu() { - const [opened, { toggle }] = useDisclosure(false); - - return ( - - - - - - - - - ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx deleted file mode 100644 index 06ce2676..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { type ProofKitRoute } from "@proofkit/cli"; -import { usePathname } from "next/navigation"; -import React from "react"; - -import classes from "./Header.module.css"; - -export default function HeaderNavLink(route: ProofKitRoute) { - const pathname = usePathname(); - - if (route.type === "function") { - return ( - - ); - } - - const isActive = route.exactMatch - ? pathname === route.href - : pathname.startsWith(route.href); - - if (route.type === "link") { - return ( - - {route.label} - - ); - } -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts deleted file mode 100644 index ded639d0..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts +++ /dev/null @@ -1 +0,0 @@ -export const headerHeight = 56; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx deleted file mode 100644 index 2de3b630..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderCenter() { - return null; -} - -export default SlotHeaderCenter; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx deleted file mode 100644 index 781fcbce..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Link from "next/link"; - -import AppLogo from "../AppLogo"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects this file to exist and - * may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderLeft() { - return ( - <> - - - - - ); -} - -export default SlotHeaderLeft; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx deleted file mode 100644 index 9943f8a0..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { primaryRoutes } from "@/app/navigation"; -import { Menu } from "@mantine/core"; -import { useRouter } from "next/navigation"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderMobileMenuContent({ - closeMenu, -}: { - closeMenu: () => void; -}) { - const router = useRouter(); - return ( - <> - {primaryRoutes.map((route) => ( - { - closeMenu(); - if (route.type === "function") { - route.onClick(); - } else if (route.type === "link") { - router.push(route.href); - } - }} - > - {route.label} - - ))} - - ); -} - -export default SlotHeaderMobileMenuContent; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx deleted file mode 100644 index 6c392c95..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { primaryRoutes } from "@/app/navigation"; -import { Group } from "@mantine/core"; - -import HeaderNavLink from "./internal/HeaderNavLink"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderRight() { - return ( - <> - - {primaryRoutes.map((route) => ( - - ))} - - - ); -} - -export default SlotHeaderRight; diff --git a/packages/cli-old/template/nextjs-mantine/src/config/env.ts b/packages/cli-old/template/nextjs-mantine/src/config/env.ts deleted file mode 100644 index 3c50ef8d..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/config/env.ts +++ /dev/null @@ -1,13 +0,0 @@ -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"), - }, - client: {}, - // For Next.js >= 13.4.4, you only need to destructure client variables: - experimental__runtimeEnv: {}, -}); diff --git a/packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css b/packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css deleted file mode 100644 index 0e2f76bb..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css +++ /dev/null @@ -1,125 +0,0 @@ -/* Add global styles here */ - -@import "tailwindcss" prefix(tw); -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -:root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply tw:border-border tw:outline-ring/50; - } - body { - @apply tw:bg-background tw:text-foreground; - } -} diff --git a/packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts b/packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts deleted file mode 100644 index 890db89c..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createTheme, type MantineColorsTuple } from "@mantine/core"; - -// generate your own set of colors here: https://mantine.dev/colors-generator -const brandColor: MantineColorsTuple = [ - "#ffebff", - "#f5d5fb", - "#e6a8f3", - "#d779eb", - "#cb51e4", - "#c337e0", - "#c029df", - "#a91cc6", - "#9715b1", - "#84099c", -]; - -export const theme = createTheme({ - primaryColor: "brand", - colors: { - brand: brandColor, - }, -}); diff --git a/packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts b/packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts deleted file mode 100644 index 7f62198a..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createSafeActionClient } from "next-safe-action"; - -export const actionClient = createSafeActionClient(); diff --git a/packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts b/packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts deleted file mode 100644 index b5aa63e3..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - showNotification, - type NotificationData, -} from "@mantine/notifications"; - -export function showErrorNotification(): void; -export function showErrorNotification(props: NotificationData): void; -export function showErrorNotification(message: string): void; -export function showErrorNotification(args?: string | NotificationData): void { - const message = - typeof args === "string" ? args : "An unexpected error occurred."; - const defaultProps = typeof args === "string" ? {} : (args ?? {}); - - showNotification({ color: "red", title: "Error", message, ...defaultProps }); -} - -export function showSuccessNotification(): void; -export function showSuccessNotification(props: NotificationData): void; -export function showSuccessNotification(message: string): void; -export function showSuccessNotification( - args?: string | NotificationData, -): void { - const message = typeof args === "string" ? args : "Success!"; - const defaultProps = typeof args === "string" ? {} : (args ?? {}); - - showNotification({ - color: "green", - title: "Success", - message, - ...defaultProps, - }); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/utils/styles.ts b/packages/cli-old/template/nextjs-mantine/src/utils/styles.ts deleted file mode 100644 index 1f4cd38e..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/utils/styles.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: any[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/cli-old/template/nextjs-mantine/tsconfig.json b/packages/cli-old/template/nextjs-mantine/tsconfig.json deleted file mode 100644 index 51d0dbce..00000000 --- a/packages/cli-old/template/nextjs-mantine/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - }, - "target": "ES2017" - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md b/packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md deleted file mode 100644 index 869eaac0..00000000 --- a/packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md +++ /dev/null @@ -1,327 +0,0 @@ -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite format` - Format and fix code automatically -- `npx ultracite lint` - Check for issues without fixing - -## Example: Error Handling -```typescript -// ✅ Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// ❌ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc b/packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc deleted file mode 100644 index 98495535..00000000 --- a/packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc +++ /dev/null @@ -1,333 +0,0 @@ ---- -description: Ultracite Rules - AI-Ready Formatter and Linter -globs: "**/*.{ts,tsx,js,jsx}" -alwaysApply: true ---- - -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite format` - Format and fix code automatically -- `npx ultracite lint` - Check for issues without fixing - -## Example: Error Handling -```typescript -// ✅ Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// ❌ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/.vscode/settings.json b/packages/cli-old/template/nextjs-shadcn/.vscode/settings.json deleted file mode 100644 index 1043bea0..00000000 --- a/packages/cli-old/template/nextjs-shadcn/.vscode/settings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[css]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[graphql]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "typescript.tsdk": "node_modules/typescript/lib", - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "emmet.showExpandedAbbreviation": "never", - "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit", - "source.organizeImports.biome": "explicit" - } -} \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/README.md b/packages/cli-old/template/nextjs-shadcn/README.md deleted file mode 100644 index 15794a3d..00000000 --- a/packages/cli-old/template/nextjs-shadcn/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# ProofKit NextJS Template - -This is a [NextJS](https://nextjs.org/) project bootstrapped with `@proofkit/cli`. Learn more at [proofkit.proof.sh](https://proofkit.proof.sh) - -## What's next? How do I make an app with this? - -While this template is designed to be a minimal starting point, the proofkit CLI will guide you through adding additional features and pages. - -To add new things to your project, simply run the `proofkit` script from the project's root directory. - -e.g. `npm run proofkit` or `pnpm proofkit` etc. - -For more information, see the full [ProofKit documentation](https://proofkit.proof.sh). - -## Project Structure - -ProofKit projects have an opinionated structure to help you get started and some conventions must be maintained to ensure that the CLI can properly inject new features and components. - -The `src` directory is the home for your application code. It is used for most things except for configuration and is organized as follows: - -- `app` - NextJS app router, where your pages and routes are defined -- `components` - Shared components used throughout the app -- `server` - Code that connects to backend databases and services that should not be exposed in the browser - -Anytime you see an `internal` folder, you should not modify any files inside. These files are maintained exclusively by the ProofKit CLI and changes to them may be overwritten. - -Anytime you see a componet file that begins with `slot-`, you _may_ modify the content, but do not rename, remove, or move them. These are desigend to be customized, but are still used by the CLI to inject additional content. If a slot is not needed by your app, you can have the compoment return `null` or an empty fragment: `<>` diff --git a/packages/cli-old/template/nextjs-shadcn/_gitignore b/packages/cli-old/template/nextjs-shadcn/_gitignore deleted file mode 100644 index 00bba9bb..00000000 --- a/packages/cli-old/template/nextjs-shadcn/_gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/packages/cli-old/template/nextjs-shadcn/biome.json b/packages/cli-old/template/nextjs-shadcn/biome.json deleted file mode 100644 index 3ac108f5..00000000 --- a/packages/cli-old/template/nextjs-shadcn/biome.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "root": false, - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": true, - "includes": ["**", "!node_modules", "!.next", "!dist", "!build"] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "style": { - "noParameterAssign": "error", - "useAsConstAssertion": "error", - "useDefaultParameterLast": "error", - "useEnumInitializers": "error", - "useSelfClosingElements": "error", - "useSingleVarDeclarator": "error", - "noUnusedTemplateLiteral": "error", - "useNumberNamespace": "error", - "noInferrableTypes": "error", - "noUselessElse": "error" - } - }, - "domains": { - "next": "recommended", - "react": "recommended" - } - }, - "assist": { - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "extends": ["ultracite"] -} diff --git a/packages/cli-old/template/nextjs-shadcn/components.json b/packages/cli-old/template/nextjs-shadcn/components.json deleted file mode 100644 index ffe928f5..00000000 --- a/packages/cli-old/template/nextjs-shadcn/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/next.config.ts b/packages/cli-old/template/nextjs-shadcn/next.config.ts deleted file mode 100644 index 4e6fb1fe..00000000 --- a/packages/cli-old/template/nextjs-shadcn/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NextConfig } from 'next'; -import '@/lib/env'; - -const nextConfig: NextConfig = { - /* config options here */ -}; - -export default nextConfig; diff --git a/packages/cli-old/template/nextjs-shadcn/package.json b/packages/cli-old/template/nextjs-shadcn/package.json deleted file mode 100644 index a61be86e..00000000 --- a/packages/cli-old/template/nextjs-shadcn/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "raw-next", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build --turbopack", - "proofkit": "proofkit", - "start": "next start", - "lint": "biome check", - "format": "biome format --write" - }, - "dependencies": { - "@radix-ui/react-slot": "^1.2.3", - "@t3-oss/env-nextjs": "^0.13.8", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.541.0", - "next": "^15.5.8", - "next-themes": "^0.4.6", - "radix-ui": "^1.4.2", - "react": "19.1.1", - "react-dom": "19.1.1", - "sonner": "^2.0.4", - "tailwind-merge": "^3.3.1" - }, - "devDependencies": { - "@biomejs/biome": "2.3.11", - "@tailwindcss/postcss": "^4", - "@types/node": "^22", - "@types/react": "^19", - "@types/react-dom": "^19", - "tailwindcss": "^4", - "tw-animate-css": "^1.3.7", - "typescript": "^5", - "ultracite": "5.4.5" - } -} diff --git a/packages/cli-old/template/nextjs-shadcn/postcss.config.mjs b/packages/cli-old/template/nextjs-shadcn/postcss.config.mjs deleted file mode 100644 index c7bcb4b1..00000000 --- a/packages/cli-old/template/nextjs-shadcn/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -const config = { - plugins: ["@tailwindcss/postcss"], -}; - -export default config; diff --git a/packages/cli-old/template/nextjs-shadcn/proofkit.json b/packages/cli-old/template/nextjs-shadcn/proofkit.json deleted file mode 100644 index 13d3916d..00000000 --- a/packages/cli-old/template/nextjs-shadcn/proofkit.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ui": "shadcn", - "envFile": ".env", - "appType": "browser", - "registryTemplates": ["utils/t3-env"] -} diff --git a/packages/cli-old/template/nextjs-shadcn/public/favicon.ico b/packages/cli-old/template/nextjs-shadcn/public/favicon.ico deleted file mode 100644 index ba9355b8d3f888ad3a93c0f254b6776a0c2aed92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 DtPcMKfsh_-A)LFM)6iMLm_0IRoy0b^RZ#+}< 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 b29HP@D9EFJDBd}ZfIn(VT8gjb%^v(OKI6J9 diff --git a/packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx b/packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx deleted file mode 100644 index 57a8452b..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import AppShell from "@/components/AppShell/internal/AppShell"; -import React from "react"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx b/packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx deleted file mode 100644 index be337605..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx +++ /dev/null @@ -1,137 +0,0 @@ -'use client'; - -import { - CheckIcon, - CopyIcon, - ExternalLinkIcon, - TerminalIcon, -} from 'lucide-react'; -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; - -function GitHubMark({ size = 20 }: { size?: number }) { - return ( - - ); -} - -function InlineSnippet({ command }: { command: string }) { - const [copied, setCopied] = useState(false); - - const onCopy = () => { - if (typeof window === 'undefined' || !navigator.clipboard?.writeText) { - return; - } - navigator.clipboard.writeText(command).then( - () => { - setCopied(true); - const timeoutInMilliseconds = 2000; - setTimeout(() => setCopied(false), timeoutInMilliseconds); - }, - () => { - // do nothing - } - ); - }; - - return ( -
-
- -
- - {command} - -
- -
-
- ); -} - -export default function Home() { - return ( -
-
-
- {/** biome-ignore lint/performance/noImgElement: just a template image */} - ProofKit -

Welcome!

- -

- This is the base template home page. To add more pages, components, - or other features, run the ProofKit CLI from within your project. -

- - - -

- To change this page, open src/app/(main)/page.tsx -

- -
-
-
-
-
- Sponsored by{' '} - - Proof - {' '} - and{' '} - - Ottomatic - -
-
- - - -
-
-
-
- ); -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/app/globals.css b/packages/cli-old/template/nextjs-shadcn/src/app/globals.css deleted file mode 100644 index dc98be74..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/app/globals.css +++ /dev/null @@ -1,122 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); -} - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/app/layout.tsx b/packages/cli-old/template/nextjs-shadcn/src/app/layout.tsx deleted file mode 100644 index 1061d26d..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/app/layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; -import Providers from "@/components/providers"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "My ProofKit App", - description: "Generated by the ProofKit CLI", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/app/navigation.tsx b/packages/cli-old/template/nextjs-shadcn/src/app/navigation.tsx deleted file mode 100644 index 887073db..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/app/navigation.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { type ProofKitRoute } from "@proofkit/cli"; - -export const primaryRoutes: ProofKitRoute[] = [ - { - label: "Dashboard", - type: "link", - href: "/", - exactMatch: true, - }, -]; - -export const secondaryRoutes: ProofKitRoute[] = []; diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppLogo.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppLogo.tsx deleted file mode 100644 index c1cd2554..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppLogo.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { InfinityIcon } from "lucide-react"; -import React from "react"; - -export default function AppLogo() { - return ; -} \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx deleted file mode 100644 index d842e03c..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import { Header } from "@/components/AppShell/internal/Header"; -import { headerHeight } from "./config"; - -export default function MainAppShell({ - children, -}: { - children: React.ReactNode; -}) { - return ( -
-
-
-
-
- {children} -
-
- ); -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css deleted file mode 100644 index 2733308e..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.header { - margin-bottom: 7.5rem; - background-color: var(--pk-header-bg, transparent); - border-bottom: 1px solid var(--pk-border, rgba(0,0,0,0.08)); -} - -.inner { - display: flex; - justify-content: space-between; - align-items: center; -} - -.link { - display: block; - line-height: 1; - padding: 0.5rem 0.75rem; - border-radius: 0.375rem; - text-decoration: none; - color: inherit; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - background: none; - border: none; -} - -.link:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -[data-theme="dark"] .link:hover { - background-color: rgba(255, 255, 255, 0.06); -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx deleted file mode 100644 index a302ecce..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import SlotHeaderCenter from "../slot-header-center"; -import SlotHeaderLeft from "../slot-header-left"; -import SlotHeaderRight from "../slot-header-right"; -import { headerHeight } from "./config"; -import classes from "./Header.module.css"; -import HeaderMobileMenu from "./HeaderMobileMenu"; - -export function Header() { - return ( -
-
-
- -
- -
-
- -
-
- -
-
-
-
- ); -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx deleted file mode 100644 index ac2a2e2b..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { useState } from "react"; -import SlotHeaderMobileMenuContent from "../slot-header-mobile-content"; - -export default function HeaderMobileMenu() { - const [opened, setOpened] = useState(false); - - return ( -
- - {opened && ( -
- setOpened(false)} /> -
- )} -
- ); -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx deleted file mode 100644 index 06ce2676..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { type ProofKitRoute } from "@proofkit/cli"; -import { usePathname } from "next/navigation"; -import React from "react"; - -import classes from "./Header.module.css"; - -export default function HeaderNavLink(route: ProofKitRoute) { - const pathname = usePathname(); - - if (route.type === "function") { - return ( - - ); - } - - const isActive = route.exactMatch - ? pathname === route.href - : pathname.startsWith(route.href); - - if (route.type === "link") { - return ( - - {route.label} - - ); - } -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/config.ts b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/config.ts deleted file mode 100644 index ded639d0..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/config.ts +++ /dev/null @@ -1 +0,0 @@ -export const headerHeight = 56; diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx deleted file mode 100644 index 2de3b630..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderCenter() { - return null; -} - -export default SlotHeaderCenter; diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx deleted file mode 100644 index 781fcbce..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Link from "next/link"; - -import AppLogo from "../AppLogo"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects this file to exist and - * may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderLeft() { - return ( - <> - - - - - ); -} - -export default SlotHeaderLeft; diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx deleted file mode 100644 index f63d0365..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { primaryRoutes } from "@/app/navigation"; -import { useRouter } from "next/navigation"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderMobileMenuContent({ - closeMenu, -}: { - closeMenu: () => void; -}) { - const router = useRouter(); - return ( -
- {primaryRoutes.map((route) => ( - - ))} -
- ); -} - -export default SlotHeaderMobileMenuContent; diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx deleted file mode 100644 index afe06352..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { primaryRoutes } from "@/app/navigation"; - -import HeaderNavLink from "./internal/HeaderNavLink"; -import { ModeToggle } from "../mode-toggle"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderRight() { - return ( -
- {primaryRoutes.map((route) => ( - - ))} - -
- ); -} - -export default SlotHeaderRight; diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/mode-toggle.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/mode-toggle.tsx deleted file mode 100644 index bff50676..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/mode-toggle.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { Moon, Sun } from "lucide-react"; -import { useTheme } from "next-themes"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -export function ModeToggle() { - const { setTheme } = useTheme(); - - return ( - - - - - - setTheme("light")}> - Light - - setTheme("dark")}> - Dark - - setTheme("system")}> - System - - - - ); -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/providers.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/providers.tsx deleted file mode 100644 index a101d447..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/providers.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { ThemeProvider } from "./theme-provider"; -import { Toaster } from "./ui/sonner"; - -export default function Providers({ children }: { children: React.ReactNode }) { - return ( - - {children} - - - ); -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/theme-provider.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/theme-provider.tsx deleted file mode 100644 index 6459132f..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/theme-provider.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import { ThemeProvider as NextThemesProvider } from "next-themes"; -import type * as React from "react"; - -export function ThemeProvider({ - children, - ...props -}: React.ComponentProps) { - return {children}; -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/ui/button.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/ui/button.tsx deleted file mode 100644 index 30f83b65..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/ui/button.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; -} - -function Button({ - className, - variant, - size, - asChild = false, - ref, - ...props -}: ButtonProps & { ref?: React.Ref }) { - const Comp = asChild ? Slot : "button"; - return ( - - ); -} - -export { Button, buttonVariants }; diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index d1c32758..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,267 +0,0 @@ -"use client"; - -import { Check, ChevronRight, Circle } from "lucide-react"; -import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function DropdownMenu({ - ...props -}: React.ComponentProps) { - return ; -} - -function DropdownMenuPortal({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuContent({ - className, - sideOffset = 4, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function DropdownMenuGroup({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuItem({ - className, - inset, - variant, - ...props -}: React.ComponentProps & { - inset?: boolean; - variant?: "destructive"; -}) { - return ( - - ); -} - -function DropdownMenuCheckboxItem({ - className, - children, - checked, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function DropdownMenuRadioItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function DropdownMenuLabel({ - className, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean; -}) { - return ( - - ); -} - -function DropdownMenuRadioGroup({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuShortcut({ - className, - ...props -}: React.HTMLAttributes) { - return ( - - ); -} - -function DropdownMenuSub({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuSubTrigger({ - className, - inset, - children, - ...props -}: React.ComponentProps & { - inset?: boolean; -}) { - return ( - svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 [&_svg:not([role=img]):not([class*=text-])]:opacity-60", - inset && "ps-8", - className - )} - data-slot="dropdown-menu-sub-trigger" - {...props} - > - {children} - - - ); -} - -function DropdownMenuSubContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuPortal, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -}; diff --git a/packages/cli-old/template/nextjs-shadcn/src/components/ui/sonner.tsx b/packages/cli-old/template/nextjs-shadcn/src/components/ui/sonner.tsx deleted file mode 100644 index 79926117..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/components/ui/sonner.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { useTheme } from "next-themes"; -import { Toaster as Sonner } from "sonner"; - -type ToasterProps = React.ComponentProps; - -function Toaster({ ...props }: ToasterProps) { - const { theme = "system" } = useTheme(); - - return ( - - ); -} - -export { Toaster }; diff --git a/packages/cli-old/template/nextjs-shadcn/src/lib/env.ts b/packages/cli-old/template/nextjs-shadcn/src/lib/env.ts deleted file mode 100644 index 83518a22..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/lib/env.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod/v4"; - -export const env = createEnv({ - server: { - NODE_ENV: z - .enum(["development", "test", "production"]) - .catch("development"), - }, - client: {}, - experimental__runtimeEnv: {}, -}); diff --git a/packages/cli-old/template/nextjs-shadcn/src/lib/utils.ts b/packages/cli-old/template/nextjs-shadcn/src/lib/utils.ts deleted file mode 100644 index bd0c391d..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/packages/cli-old/template/nextjs-shadcn/tsconfig.json b/packages/cli-old/template/nextjs-shadcn/tsconfig.json deleted file mode 100644 index dd41d9d9..00000000 --- a/packages/cli-old/template/nextjs-shadcn/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": [ - "./src/*" - ] - }, - "strictNullChecks": true - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file diff --git a/packages/cli-old/template/pages/nextjs/blank/page.tsx b/packages/cli-old/template/pages/nextjs/blank/page.tsx deleted file mode 100644 index dcdbd2be..00000000 --- a/packages/cli-old/template/pages/nextjs/blank/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export default function BlankPage() { - return
BlankPage
; -} diff --git a/packages/cli-old/template/pages/nextjs/table-edit/actions.ts b/packages/cli-old/template/pages/nextjs/table-edit/actions.ts deleted file mode 100644 index 20dcfa92..00000000 --- a/packages/cli-old/template/pages/nextjs/table-edit/actions.ts +++ /dev/null @@ -1,24 +0,0 @@ -"use server"; - -import { __ZOD_TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { __ACTION_CLIENT__ } from "@/server/safe-action"; - -import { idFieldName } from "./schema"; - -export const updateRecord = __ACTION_CLIENT__ - .inputSchema(__ZOD_TYPE_NAME__.partial()) - .action(async ({ parsedInput }) => { - const id = parsedInput[idFieldName]; - delete parsedInput[idFieldName]; // this ensures the id field value is not included in the updated fieldData - const data = parsedInput; - - const { - data: { recordId }, - } = await __CLIENT_NAME__.findOne({ query: { [idFieldName]: `==${id}` } }); - - return await __CLIENT_NAME__.update({ - recordId, - fieldData: data, - }); - }); diff --git a/packages/cli-old/template/pages/nextjs/table-edit/page.tsx b/packages/cli-old/template/pages/nextjs/table-edit/page.tsx deleted file mode 100644 index 5957658b..00000000 --- a/packages/cli-old/template/pages/nextjs/table-edit/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { Code, Stack, Text } from "@mantine/core"; -import React from "react"; - -import { idFieldName } from "./schema"; -import TableContent from "./table"; - -export default async function TablePage() { - // this function is limited to 100 records by default. To load more, see the other table templates from the docs - const { data } = await __CLIENT_NAME__.list({ - fetch: { next: { revalidate: 60 } }, // only call the database at most once every 60 seconds - }); - return ( - -
- - This table allows editing. Double-click on a cell to edit the value. - - - NOTE: This feature requires a primary key field on your API layout. If - your primary key field is not {idFieldName}, update the - idFieldName variable in the schema.ts file. - -
- d.fieldData)} /> -
- ); -} diff --git a/packages/cli-old/template/pages/nextjs/table-edit/schema.ts b/packages/cli-old/template/pages/nextjs/table-edit/schema.ts deleted file mode 100644 index 28c55f5c..00000000 --- a/packages/cli-old/template/pages/nextjs/table-edit/schema.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { type __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; - -// TODO: Make sure this variable is properly set to your primary key field -export const idFieldName: keyof __TYPE_NAME__ = "__FIRST_FIELD_NAME__"; diff --git a/packages/cli-old/template/pages/nextjs/table-edit/table.tsx b/packages/cli-old/template/pages/nextjs/table-edit/table.tsx deleted file mode 100644 index 2166994f..00000000 --- a/packages/cli-old/template/pages/nextjs/table-edit/table.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { type __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { showErrorNotification } from "@/utils/notification-helpers"; -import { - MantineReactTable, - useMantineReactTable, - type MRT_Cell, - type MRT_ColumnDef, -} from "mantine-react-table"; -import React from "react"; - -import { updateRecord } from "./actions"; -import { idFieldName } from "./schema"; - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -async function handleSaveCell(cell: MRT_Cell, value: unknown) { - const resp = await updateRecord({ - [idFieldName]: cell.row.id, - [cell.column.id]: value, - }); - if (!resp?.data) { - showErrorNotification("Failed to update record"); - } -} - -export default function MyTable({ data }: { data: TData[] }) { - const table = useMantineReactTable({ - data, - columns, - enableEditing: true, - editDisplayMode: "cell", - getRowId: (row) => row[idFieldName], - mantineEditTextInputProps: ({ cell }) => ({ - //onBlur is more efficient, but could use onChange instead - onBlur: (event) => { - handleSaveCell(cell, event.target.value); - }, - }), - }); - return ; -} diff --git a/packages/cli-old/template/pages/nextjs/table-infinite-edit/actions.ts b/packages/cli-old/template/pages/nextjs/table-infinite-edit/actions.ts deleted file mode 100644 index db312817..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite-edit/actions.ts +++ /dev/null @@ -1,84 +0,0 @@ -"use server"; - -import { - __TYPE_NAME__, - __ZOD_TYPE_NAME__, -} from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { __ACTION_CLIENT__ } from "@/server/safe-action"; -import { clientTypes } from "@proofkit/fmdapi"; -import dayjs from "dayjs"; -import { z } from "zod/v4"; - -import { idFieldName } from "./schema"; - -const limit = 50; // raise or lower this number depending on how your layout performs -export const fetchData = __ACTION_CLIENT__ - .inputSchema( - z.object({ - offset: z.number().catch(0), - sorting: z.array( - z.object({ id: z.string(), desc: z.boolean().default(false) }) - ), - columnFilters: z.array(z.object({ id: z.string(), value: z.unknown() })), - }) - ) - .action(async ({ parsedInput: { offset, sorting, columnFilters } }) => { - - const getOptions: clientTypes.ListParams<__TYPE_NAME__, any> & { - query: clientTypes.Query<__TYPE_NAME__>[]; - } = { - limit, - offset, - query: [{ ["__FIRST_FIELD_NAME__"]: "*" }], - }; - - if (sorting.length > 0) { - getOptions.sort = sorting.map(({ id, desc }) => ({ - fieldName: id as keyof __TYPE_NAME__, - sortOrder: desc ? "descend" : "ascend", - })); - } - - if (columnFilters.length > 0) { - getOptions.query = columnFilters - .map(({ id, value }) => { - if (typeof value === "string") { - return { - [id]: value, - }; - } else if (typeof value === "object" && value instanceof Date) { - return { - [id]: dayjs(value).format("YYYY+MM+DD"), - }; - } - return null; - }) - .filter(Boolean) as clientTypes.Query[]; - } - - const data = await __CLIENT_NAME__.find(getOptions); - - return { - data: data.data, - hasNextPage: data.dataInfo.foundCount > limit + offset, - totalCount: data.dataInfo.foundCount, - }; - }); - -export const updateRecord = __ACTION_CLIENT__ - .inputSchema(__ZOD_TYPE_NAME__.partial()) - .action(async ({ parsedInput }) => { - const id = parsedInput[idFieldName]; - delete parsedInput[idFieldName]; // this ensures the id field value is not included in the updated fieldData - const data = parsedInput; - - const { - data: { recordId }, - } = await __CLIENT_NAME__.findOne({ query: { [idFieldName]: `==${id}` } }); - - return await __CLIENT_NAME__.update({ - recordId, - fieldData: data, - }); - }); diff --git a/packages/cli-old/template/pages/nextjs/table-infinite-edit/page.tsx b/packages/cli-old/template/pages/nextjs/table-infinite-edit/page.tsx deleted file mode 100644 index d194b139..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite-edit/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Code, Stack, Text } from "@mantine/core"; -import React from "react"; - -import { idFieldName } from "./schema"; -import TableContent from "./table"; - -export default async function TablePage() { - return ( - -
- - This table allows editing. Double-click on a cell to edit the value. - - - NOTE: This feature requires a primary key field on your API layout. If - your primary key field is not {idFieldName}, update the - idFieldName variable in the schema.ts file. - -
- -
- ); -} diff --git a/packages/cli-old/template/pages/nextjs/table-infinite-edit/query.ts b/packages/cli-old/template/pages/nextjs/table-infinite-edit/query.ts deleted file mode 100644 index faf1675a..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite-edit/query.ts +++ /dev/null @@ -1,87 +0,0 @@ -"use client"; - -import { showErrorNotification } from "@/utils/notification-helpers"; -import { - useInfiniteQuery, - useMutation, - useQueryClient, -} from "@tanstack/react-query"; -import type { - MRT_ColumnFiltersState, - MRT_SortingState, -} from "mantine-react-table"; -import { useMemo } from "react"; - -import { fetchData, updateRecord } from "./actions"; -import { idFieldName } from "./schema"; - -export function useAllData({ - sorting, - columnFilters, -}: { - sorting: MRT_SortingState; - columnFilters: MRT_ColumnFiltersState; -}) { - const queryKey = ["all-__SCHEMA_NAME__", sorting, columnFilters]; - // useInfiniteQuery is used to help with automatic pagination - const qr = useInfiniteQuery({ - queryKey, - queryFn: async ({ pageParam: offset }) => { - const result = await fetchData({ offset, sorting, columnFilters }); - if (!result) throw new Error(`Failed to fetch __SCHEMA_NAME__`); - if (!result.data) throw new Error(`No data found for __SCHEMA_NAME__`); - return result?.data; - }, - retry: false, - initialPageParam: 0, - getNextPageParam: (lastPage, pages) => - lastPage.hasNextPage - ? pages.flatMap((page) => page.data).length - : undefined, - }); - - const flatData = useMemo( - () => - qr.data?.pages.flatMap((page) => page.data).map((o) => o.fieldData) ?? [], - [qr.data] - ); - const totalFetched = flatData.length; - const totalDBRowCount = qr.data?.pages?.[0]?.totalCount ?? 0; - - const queryClient = useQueryClient(); - - const updateRecordMutation = useMutation({ - mutationFn: updateRecord, - onMutate: async (newRecord) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ queryKey }); - - // Optimistically update to the new value - queryClient.setQueryData(queryKey, (old) => { - if (!old) return old; - return { - ...old, - pages: old.pages.map((page) => ({ - ...page, - data: page.data.map((row) => - row.fieldData[idFieldName] === newRecord[idFieldName] - ? { ...row, fieldData: { ...row.fieldData, ...newRecord } } - : row - ), - })), - }; - }); - }, - onError: () => { - showErrorNotification("Failed to update record"); - }, - }); - - return { - ...qr, - data: flatData, - totalDBRowCount, - totalFetched, - updateRecord: updateRecordMutation.mutate, - }; -} diff --git a/packages/cli-old/template/pages/nextjs/table-infinite-edit/schema.ts b/packages/cli-old/template/pages/nextjs/table-infinite-edit/schema.ts deleted file mode 100644 index 28c55f5c..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite-edit/schema.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { type __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; - -// TODO: Make sure this variable is properly set to your primary key field -export const idFieldName: keyof __TYPE_NAME__ = "__FIRST_FIELD_NAME__"; diff --git a/packages/cli-old/template/pages/nextjs/table-infinite-edit/table.tsx b/packages/cli-old/template/pages/nextjs/table-infinite-edit/table.tsx deleted file mode 100644 index aeb29899..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite-edit/table.tsx +++ /dev/null @@ -1,130 +0,0 @@ -"use client"; - -import { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { Text } from "@mantine/core"; -import { - createMRTColumnHelper, - MantineReactTable, - useMantineReactTable, - type MRT_Cell, - type MRT_ColumnDef, - type MRT_ColumnFiltersState, - type MRT_RowVirtualizer, - type MRT_SortingState, -} from "mantine-react-table"; -import React, { - useCallback, - useEffect, - useRef, - useState, - type UIEvent, -} from "react"; - -import { useAllData } from "./query"; -import { idFieldName } from "./schema"; - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -export default function MyTable() { - const tableContainerRef = useRef(null); - const rowVirtualizerInstanceRef = - useRef>(null); - - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState( - [] - ); - - const { - data, - totalDBRowCount, - totalFetched, - isLoading, - isFetching, - fetchNextPage, - updateRecord, - } = useAllData({ sorting, columnFilters }); - - async function handleSaveCell(cell: MRT_Cell, value: unknown) { - updateRecord({ - [idFieldName]: cell.row.id, - [cell.column.id]: value, - }); - } - - const table = useMantineReactTable({ - data, - columns, - rowCount: totalDBRowCount, - enableRowVirtualization: true, // only render the rows that are visible on screen to improve performance - state: { isLoading, sorting, showProgressBars: isFetching, columnFilters }, - enableGlobalFilter: false, // doesn't work as easily with server-side filters, it's better to filter the specific columns - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - enablePagination: false, // hide pagination buttons - enableStickyHeader: true, - mantineBottomToolbarProps: { style: { alignItems: "center" } }, - renderBottomToolbarCustomActions: () => - !isLoading ? ( - - Fetched {totalFetched} of {totalDBRowCount} - - ) : null, - mantineTableContainerProps: ({ table }) => { - return { - h: table.getState().isFullScreen - ? "100%" - : `calc(100vh - var(--app-shell-header-height) - 13rem)`, // may need to adjust this height if you have more elements on your page - ref: tableContainerRef, - onScroll: ( - event: UIEvent //add an event listener to the table container element - ) => fetchMoreOnBottomReached(event.target as HTMLDivElement), - }; - }, - - /** Inline editing functionality */ - enableEditing: true, - editDisplayMode: "cell", - getRowId: (row) => row[idFieldName], - mantineEditTextInputProps: ({ cell }) => ({ - // onBlur is more efficient (only called when you leave the field) - // onChange event could be used for other types of edits, like dropdowns - onBlur: (event) => { - handleSaveCell(cell, event.target.value); - }, - }), - }); - - // called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table - const fetchMoreOnBottomReached = useCallback( - (containerRefElement?: HTMLDivElement | null) => { - if (containerRefElement) { - const { scrollHeight, scrollTop, clientHeight } = containerRefElement; - // once the user has scrolled within 400px of the bottom of the table, fetch more data - if ( - scrollHeight - scrollTop - clientHeight < 400 && - !isFetching && - totalFetched < totalDBRowCount - ) { - void fetchNextPage(); - } - } - }, - [fetchNextPage, isFetching, totalFetched, totalDBRowCount] - ); - - // scroll to top of table when sorting or filters change - useEffect(() => { - if (rowVirtualizerInstanceRef.current) { - try { - rowVirtualizerInstanceRef.current.scrollToIndex(0); - } catch (e) { - console.error(e); - } - } - }, [sorting, columnFilters]); - - return ; -} diff --git a/packages/cli-old/template/pages/nextjs/table-infinite/actions.ts b/packages/cli-old/template/pages/nextjs/table-infinite/actions.ts deleted file mode 100644 index 68f30627..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite/actions.ts +++ /dev/null @@ -1,62 +0,0 @@ -"use server"; - -import { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { __ACTION_CLIENT__ } from "@/server/safe-action"; -import { clientTypes } from "@proofkit/fmdapi"; -import dayjs from "dayjs"; -import { z } from "zod/v4"; - -const limit = 50; // raise or lower this number depending on how your layout performs -export const fetchData = __ACTION_CLIENT__ - .inputSchema( - z.object({ - offset: z.number().catch(0), - sorting: z.array( - z.object({ id: z.string(), desc: z.boolean().default(false) }) - ), - columnFilters: z.array(z.object({ id: z.string(), value: z.unknown() })), - }) - ) - .action(async ({ parsedInput: { offset, sorting, columnFilters } }) => { - - const getOptions: clientTypes.ListParams<__TYPE_NAME__, any> & { - query: clientTypes.Query<__TYPE_NAME__>[]; - } = { - limit, - offset, - query: [{ ["__FIRST_FIELD_NAME__"]: "*" }], - }; - - if (sorting.length > 0) { - getOptions.sort = sorting.map(({ id, desc }) => ({ - fieldName: id as keyof __TYPE_NAME__, - sortOrder: desc ? "descend" : "ascend", - })); - } - - if (columnFilters.length > 0) { - getOptions.query = columnFilters - .map(({ id, value }) => { - if (typeof value === "string") { - return { - [id]: value, - }; - } else if (typeof value === "object" && value instanceof Date) { - return { - [id]: dayjs(value).format("YYYY+MM+DD"), - }; - } - return null; - }) - .filter(Boolean) as clientTypes.Query[]; - } - - const data = await __CLIENT_NAME__.find(getOptions); - - return { - data: data.data, - hasNextPage: data.dataInfo.foundCount > limit + offset, - totalCount: data.dataInfo.foundCount, - }; - }); diff --git a/packages/cli-old/template/pages/nextjs/table-infinite/page.tsx b/packages/cli-old/template/pages/nextjs/table-infinite/page.tsx deleted file mode 100644 index 990ef802..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Stack } from "@mantine/core"; - -import MyTable from "./table"; - -export default async function TablePage() { - return ( - - - - ); -} diff --git a/packages/cli-old/template/pages/nextjs/table-infinite/query.ts b/packages/cli-old/template/pages/nextjs/table-infinite/query.ts deleted file mode 100644 index d4c0e26a..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite/query.ts +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { useInfiniteQuery } from "@tanstack/react-query"; -import type { - MRT_ColumnFiltersState, - MRT_SortingState, -} from "mantine-react-table"; -import { useMemo } from "react"; - -import { fetchData } from "./actions"; - -export function useAllData({ - sorting, - columnFilters, -}: { - sorting: MRT_SortingState; - columnFilters: MRT_ColumnFiltersState; -}) { - // useInfiniteQuery is used to help with automatic pagination - const qr = useInfiniteQuery({ - queryKey: ["all-__SCHEMA_NAME__", sorting, columnFilters], - queryFn: async ({ pageParam: offset }) => { - const result = await fetchData({ offset, sorting, columnFilters }); - if (!result) throw new Error(`Failed to fetch __SCHEMA_NAME__`); - if (!result.data) throw new Error(`No data found for __SCHEMA_NAME__`); - return result?.data; - }, - retry: false, - initialPageParam: 0, - getNextPageParam: (lastPage, pages) => - lastPage.hasNextPage - ? pages.flatMap((page) => page.data).length - : undefined, - }); - - const flatData = useMemo( - () => - qr.data?.pages.flatMap((page) => page.data).map((o) => o.fieldData) ?? [], - [qr.data] - ); - const totalFetched = flatData.length; - const totalDBRowCount = qr.data?.pages?.[0]?.totalCount ?? 0; - - return { ...qr, data: flatData, totalDBRowCount, totalFetched }; -} diff --git a/packages/cli-old/template/pages/nextjs/table-infinite/table.tsx b/packages/cli-old/template/pages/nextjs/table-infinite/table.tsx deleted file mode 100644 index d76daa43..00000000 --- a/packages/cli-old/template/pages/nextjs/table-infinite/table.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { Text } from "@mantine/core"; -import { - createMRTColumnHelper, - MantineReactTable, - MRT_ColumnDef, - MRT_ColumnFiltersState, - MRT_RowVirtualizer, - MRT_SortingState, - useMantineReactTable, -} from "mantine-react-table"; -import React, { - useCallback, - useEffect, - useRef, - useState, - type UIEvent, -} from "react"; - -import { useAllData } from "./query"; - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -export default function MyTable() { - const tableContainerRef = useRef(null); - const rowVirtualizerInstanceRef = - useRef>(null); - - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState( - [] - ); - - const { - data, - totalDBRowCount, - totalFetched, - isLoading, - isFetching, - fetchNextPage, - } = useAllData({ sorting, columnFilters }); - - const table = useMantineReactTable({ - data, - columns, - rowCount: totalDBRowCount, - enableRowVirtualization: true, // only render the rows that are visible on screen to improve performance - state: { isLoading, sorting, showProgressBars: isFetching, columnFilters }, - enableGlobalFilter: false, // doesn't work as easily with server-side filters, it's better to filter the specific columns - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - enablePagination: false, // hide pagination buttons - enableStickyHeader: true, - mantineBottomToolbarProps: { style: { alignItems: "center" } }, - renderBottomToolbarCustomActions: () => - !isLoading ? ( - - Fetched {totalFetched} of {totalDBRowCount} - - ) : null, - mantineTableContainerProps: ({ table }) => { - return { - h: table.getState().isFullScreen - ? "100%" - : `calc(100vh - var(--app-shell-header-height) - 10rem)`, // may need to adjust this height if you have more elements on your page - ref: tableContainerRef, - onScroll: ( - event: UIEvent //add an event listener to the table container element - ) => fetchMoreOnBottomReached(event.target as HTMLDivElement), - }; - }, - }); - - // called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table - const fetchMoreOnBottomReached = useCallback( - (containerRefElement?: HTMLDivElement | null) => { - if (containerRefElement) { - const { scrollHeight, scrollTop, clientHeight } = containerRefElement; - // once the user has scrolled within 400px of the bottom of the table, fetch more data - if ( - scrollHeight - scrollTop - clientHeight < 400 && - !isFetching && - totalFetched < totalDBRowCount - ) { - void fetchNextPage(); - } - } - }, - [fetchNextPage, isFetching, totalFetched, totalDBRowCount] - ); - - // scroll to top of table when sorting or filters change - useEffect(() => { - if (rowVirtualizerInstanceRef.current) { - try { - rowVirtualizerInstanceRef.current.scrollToIndex(0); - } catch (e) { - console.error(e); - } - } - }, [sorting, columnFilters]); - - return ; -} diff --git a/packages/cli-old/template/pages/nextjs/table/page.tsx b/packages/cli-old/template/pages/nextjs/table/page.tsx deleted file mode 100644 index da3dae96..00000000 --- a/packages/cli-old/template/pages/nextjs/table/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { Stack } from "@mantine/core"; -import React from "react"; - -import TableContent from "./table"; - -export default async function TablePage() { - // this function is limited to 100 records by default. To load more, see the other table templates from the docs - const { data } = await __CLIENT_NAME__.list({ - fetch: { next: { revalidate: 60 } }, // only call the database at most once every 60 seconds - }); - return ( - - d.fieldData)} /> - - ); -} diff --git a/packages/cli-old/template/pages/nextjs/table/table.tsx b/packages/cli-old/template/pages/nextjs/table/table.tsx deleted file mode 100644 index 52479327..00000000 --- a/packages/cli-old/template/pages/nextjs/table/table.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { - MantineReactTable, - MRT_ColumnDef, - useMantineReactTable, -} from "mantine-react-table"; -import React from "react"; - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -export default function MyTable({ data }: { data: TData[] }) { - const table = useMantineReactTable({ data, columns }); - return ; -} diff --git a/packages/cli-old/template/pages/vite-wv/blank/index.tsx b/packages/cli-old/template/pages/vite-wv/blank/index.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/cli-old/template/pages/vite-wv/table-edit/index.tsx b/packages/cli-old/template/pages/vite-wv/table-edit/index.tsx deleted file mode 100644 index 0db5cb10..00000000 --- a/packages/cli-old/template/pages/vite-wv/table-edit/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import FullScreenLoader from "@/components/full-screen-loader"; -import { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { Code, Stack, Text } from "@mantine/core"; -import { createFileRoute } from "@tanstack/react-router"; -import { - MantineReactTable, - MRT_Cell, - MRT_ColumnDef, - useMantineReactTable, -} from "mantine-react-table"; - -export const Route = createFileRoute("/")({ - component: RouteComponent, - pendingComponent: () => , - loader: async () => { - // this function is limited to 100 records by default. To load more, see the other table templates from the docs - const { data } = await __CLIENT_NAME__.list(); - return data.map((record) => record.fieldData) as __TYPE_NAME__[]; - }, -}); - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -// TODO: Make sure this variable is properly set to your primary key field -const idFieldName: keyof __TYPE_NAME__ = "__FIRST_FIELD_NAME__"; -async function handleSaveCell(cell: MRT_Cell, value: unknown) { - const { - data: { recordId }, - } = await __CLIENT_NAME__.findOne({ - query: { [idFieldName]: `==${cell.row.id}` }, - }); - - await __CLIENT_NAME__.update({ - fieldData: { [cell.column.id]: value }, - recordId, - }); -} - -function RouteComponent() { - const data = Route.useLoaderData(); - const table = useMantineReactTable({ - data, - columns, - enableEditing: true, - editDisplayMode: "cell", - getRowId: (row) => row[idFieldName], - mantineEditTextInputProps: ({ cell }) => ({ - //onBlur is more efficient, but could use onChange instead - onBlur: (event) => { - handleSaveCell(cell, event.target.value); - }, - }), - }); - return ( - -
- - This table allows editing. Double-click on a cell to edit the value. - - - NOTE: This feature requires a primary key field on your API layout. If - your primary key field is not {idFieldName}, update the - idFieldName variable in the code. - -
- -
- ); -} diff --git a/packages/cli-old/template/pages/vite-wv/table/index.tsx b/packages/cli-old/template/pages/vite-wv/table/index.tsx deleted file mode 100644 index 6f09d89c..00000000 --- a/packages/cli-old/template/pages/vite-wv/table/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import FullScreenLoader from "@/components/full-screen-loader"; -import { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { Stack, Text } from "@mantine/core"; -import { createFileRoute } from "@tanstack/react-router"; -import { - MantineReactTable, - MRT_ColumnDef, - useMantineReactTable, -} from "mantine-react-table"; - -export const Route = createFileRoute("/")({ - component: RouteComponent, - pendingComponent: () => , - loader: async () => { - // this function is limited to 100 records by default. To load more, see the other table templates from the docs - const { data } = await __CLIENT_NAME__.list(); - return data.map((record) => record.fieldData) as __TYPE_NAME__[]; - }, -}); - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -function RouteComponent() { - const data = Route.useLoaderData(); - const table = useMantineReactTable({ data, columns }); - return ( - - This basic table loads up to 100 records by default - - - ); -} diff --git a/packages/cli-old/template/vite-wv/.claude/launch.json b/packages/cli-old/template/vite-wv/.claude/launch.json deleted file mode 100644 index 469bea0f..00000000 --- a/packages/cli-old/template/vite-wv/.claude/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "configurations": [ - { - "name": "Preview", - "runtimeExecutable": "__PACKAGE_MANAGER__", - "runtimeArgs": ["run", "dev"], - "cwd": "${workspaceFolder}", - "autoPort": true, - "port": 5175 - }, - { - "name": "Typegen", - "runtimeExecutable": "__PACKAGE_MANAGER__", - "runtimeArgs": ["run", "typegen"], - "cwd": "${workspaceFolder}" - } - ] -} diff --git a/packages/cli-old/template/vite-wv/.vscode/settings.json b/packages/cli-old/template/vite-wv/.vscode/settings.json deleted file mode 100644 index 00b5278e..00000000 --- a/packages/cli-old/template/vite-wv/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "files.watcherExclude": { - "**/routeTree.gen.ts": true - }, - "search.exclude": { - "**/routeTree.gen.ts": true - }, - "files.readonlyInclude": { - "**/routeTree.gen.ts": true - } -} diff --git a/packages/cli-old/template/vite-wv/AGENTS.md b/packages/cli-old/template/vite-wv/AGENTS.md deleted file mode 100644 index 6b2924ba..00000000 --- a/packages/cli-old/template/vite-wv/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -__AGENT_INSTRUCTIONS__ diff --git a/packages/cli-old/template/vite-wv/CLAUDE.md b/packages/cli-old/template/vite-wv/CLAUDE.md deleted file mode 100644 index c3170642..00000000 --- a/packages/cli-old/template/vite-wv/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md diff --git a/packages/cli-old/template/vite-wv/_gitignore b/packages/cli-old/template/vite-wv/_gitignore deleted file mode 100644 index 984db15a..00000000 --- a/packages/cli-old/template/vite-wv/_gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# Local -.DS_Store -*.local -*.log* -.env* - -# Dist -node_modules -dist/ -.vinxi -.output -.vercel -.netlify -.wrangler - -# IDE -.vscode/* -!.vscode/extensions.json -.idea diff --git a/packages/cli-old/template/vite-wv/components.json b/packages/cli-old/template/vite-wv/components.json deleted file mode 100644 index 13e1db0b..00000000 --- a/packages/cli-old/template/vite-wv/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/index.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/packages/cli-old/template/vite-wv/index.html b/packages/cli-old/template/vite-wv/index.html deleted file mode 100644 index db0fcdc2..00000000 --- a/packages/cli-old/template/vite-wv/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - ProofKit Web Viewer Starter - - - -
- - - diff --git a/packages/cli-old/template/vite-wv/package.json b/packages/cli-old/template/vite-wv/package.json deleted file mode 100644 index 13a1ee7e..00000000 --- a/packages/cli-old/template/vite-wv/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "webviewer-demo", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "build": "vite build", - "build:upload": "__PNPM_COMMAND__ build && __PNPM_COMMAND__ upload", - "dev": "vite", - "launch-fm": "node ./scripts/launch-fm.js", - "proofkit": "proofkit", - "serve": "vite preview", - "start": "vite", - "typegen": "typegen", - "typegen:ui": "typegen ui", - "upload": "node ./scripts/upload.js", - "lint": "ultracite check .", - "format": "ultracite fix ." - }, - "dependencies": { - "@tanstack/react-query": "^5.90.21", - "@tanstack/react-router": "^1.167.4", - "react": "^19.2.4", - "react-dom": "^19.2.4" - }, - "devDependencies": { - "@proofkit/typegen": "^1.1.0-beta.16", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.2.0", - "dotenv": "^17.3.1", - "open": "^11.0.0", - "typescript": "^5.9.3", - "ultracite": "7.0.8", - "vite": "^7.3.1", - "vite-plugin-singlefile": "^2.3.2" - } -} diff --git a/packages/cli-old/template/vite-wv/proofkit-typegen.config.jsonc b/packages/cli-old/template/vite-wv/proofkit-typegen.config.jsonc deleted file mode 100644 index 09274c97..00000000 --- a/packages/cli-old/template/vite-wv/proofkit-typegen.config.jsonc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://proofkit.proof.sh/typegen-config-schema.json", - "config": { - "type": "fmdapi", - "path": "./src/config/schemas/filemaker", - "clearOldFiles": true, - "clientSuffix": "Layout", - "validator": "zod/v4", - "webviewerScriptName": "ExecuteDataApi", - "fmMcp": { - "enabled": true - }, - "layouts": [ - // Add layouts here when you're ready to generate clients. - // { "layoutName": "API_Customers", "schemaName": "Customers" } - ] - } -} diff --git a/packages/cli-old/template/vite-wv/proofkit.json b/packages/cli-old/template/vite-wv/proofkit.json deleted file mode 100644 index 3bd029c2..00000000 --- a/packages/cli-old/template/vite-wv/proofkit.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ui": "shadcn", - "auth": { "type": "none" }, - "envFile": ".env", - "appType": "webviewer", - "dataSources": [], - "replacedMainPage": false, - "registryTemplates": [] -} diff --git a/packages/cli-old/template/vite-wv/scripts/filemaker.js b/packages/cli-old/template/vite-wv/scripts/filemaker.js deleted file mode 100644 index b43f2a0f..00000000 --- a/packages/cli-old/template/vite-wv/scripts/filemaker.js +++ /dev/null @@ -1,96 +0,0 @@ -import { resolve } from "node:path"; -import dotenv from "dotenv"; -import { fileURLToPath } from "node:url"; - -const currentDirectory = fileURLToPath(new URL(".", import.meta.url)); -const envPath = resolve(currentDirectory, "../.env"); - -dotenv.config({ path: envPath }); - -const defaultFmMcpBaseUrl = process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365"; - -function stripFileExtension(fileName) { - return fileName.replace(/\.fmp12$/i, ""); -} - -async function getConnectedFiles(baseUrl = defaultFmMcpBaseUrl) { - const healthResponse = await fetch(`${baseUrl}/health`).catch(() => null); - if (!healthResponse?.ok) { - return []; - } - - const connectedFiles = await fetch(`${baseUrl}/connectedFiles`) - .then((response) => (response.ok ? response.json() : [])) - .catch(() => []); - - return Array.isArray(connectedFiles) ? connectedFiles : []; -} - -function normalizeTarget(fileName) { - return stripFileExtension(fileName).toLowerCase(); -} - -export async function resolveFileMakerTarget() { - const connectedFiles = await getConnectedFiles(); - const targetFromEnv = process.env.FM_DATABASE ? normalizeTarget(process.env.FM_DATABASE) : undefined; - - if (targetFromEnv) { - const matches = connectedFiles.filter((connectedFile) => normalizeTarget(connectedFile) === targetFromEnv); - if (matches.length === 1) { - return { - fileName: stripFileExtension(matches[0]), - host: "$", - source: "fm-mcp", - }; - } - - if (connectedFiles.length > 0) { - throw new Error( - `FM_DATABASE is set to "${process.env.FM_DATABASE}" but no matching connected file was found via FM MCP.`, - ); - } - } - - if (connectedFiles.length === 1) { - return { - fileName: stripFileExtension(connectedFiles[0]), - host: "$", - source: "fm-mcp", - }; - } - - if (connectedFiles.length > 1) { - throw new Error( - `Multiple FileMaker files are connected via FM MCP (${connectedFiles.join(", ")}). Set FM_DATABASE to choose one.`, - ); - } - - const serverValue = process.env.FM_SERVER; - const databaseValue = process.env.FM_DATABASE; - - if (serverValue && databaseValue) { - let hostname; - try { - hostname = new URL(serverValue).hostname; - } catch { - hostname = serverValue.replace(/^https?:\/\//, "").replace(/\/.*$/, ""); - } - - return { - fileName: stripFileExtension(databaseValue), - host: hostname, - source: "env", - }; - } - - return null; -} - -export function buildFmpUrl({ host, fileName, scriptName, parameter }) { - const params = new URLSearchParams({ script: scriptName }); - if (parameter) { - params.set("param", parameter); - } - - return `fmp://${host}/${encodeURIComponent(fileName)}?${params.toString()}`; -} diff --git a/packages/cli-old/template/vite-wv/scripts/launch-fm.js b/packages/cli-old/template/vite-wv/scripts/launch-fm.js deleted file mode 100644 index a7e2b717..00000000 --- a/packages/cli-old/template/vite-wv/scripts/launch-fm.js +++ /dev/null @@ -1,19 +0,0 @@ -import open from "open"; -import { buildFmpUrl, resolveFileMakerTarget } from "./filemaker.js"; - -const target = await resolveFileMakerTarget(); - -if (!target) { - console.error( - "Could not resolve a FileMaker file. Start the local FM MCP proxy with a connected file, or set FM_SERVER and FM_DATABASE in .env.", - ); - process.exit(1); -} - -await open( - buildFmpUrl({ - host: target.host, - fileName: target.fileName, - scriptName: "Launch Web Viewer for Dev", - }), -); diff --git a/packages/cli-old/template/vite-wv/scripts/upload.js b/packages/cli-old/template/vite-wv/scripts/upload.js deleted file mode 100644 index c9b7f6a4..00000000 --- a/packages/cli-old/template/vite-wv/scripts/upload.js +++ /dev/null @@ -1,24 +0,0 @@ -import open from "open"; -import { resolve } from "path"; -import { fileURLToPath } from "url"; -import { buildFmpUrl, resolveFileMakerTarget } from "./filemaker.js"; - -const currentDirectory = fileURLToPath(new URL(".", import.meta.url)); -const thePath = resolve(currentDirectory, "../dist", "index.html"); -const target = await resolveFileMakerTarget(); - -if (!target) { - console.error( - "Could not resolve a FileMaker file. Start the local FM MCP proxy with a connected file, or set FM_SERVER and FM_DATABASE in .env.", - ); - process.exit(1); -} - -await open( - buildFmpUrl({ - host: target.host, - fileName: target.fileName, - scriptName: "UploadWebviewerWidget", - parameter: thePath, - }), -); diff --git a/packages/cli-old/template/vite-wv/src/App.tsx b/packages/cli-old/template/vite-wv/src/App.tsx deleted file mode 100644 index 82be44fb..00000000 --- a/packages/cli-old/template/vite-wv/src/App.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { globalSettings } from "@proofkit/webviewer"; -import type { LucideIcon } from "lucide-react"; -import { Database, Layers, Sparkles } from "lucide-react"; - -type Step = { - readonly icon: LucideIcon; - readonly title: string; - readonly body: string; -}; - -globalSettings.setWebViewerName("web"); - -const steps: readonly Step[] = [ - { - icon: Database, - title: "Connect FileMaker later", - body: "This starter renders safely in a normal browser. When you are ready, wire in FM MCP or hosted FileMaker setup with ProofKit commands.", - }, - { - icon: Layers, - title: "Generate clients when ready", - body: "Add layouts to proofkit-typegen.config.jsonc, then run your typegen script to create strongly typed layout clients.", - }, - { - icon: Sparkles, - title: "Add shadcn components fast", - body: "Tailwind v4 and shadcn are already initialized, so agents and developers can add components without extra setup.", - }, -] as const; - -export default function App() { - return ( -
-
-
-
- - ProofKit Web Viewer Starter -
- -
-
-

- React + TypeScript + Vite -

-

- Build browser-safe FileMaker Web Viewer apps without scaffolding against a hosted server. -

-

- This starter stays intentionally small, but it is already ready for Tailwind v4, shadcn component - installs, hash-based TanStack Router navigation, React Query, and later ProofKit typegen output. -

- -
- pnpm dev - pnpm typegen - pnpm launch-fm -
-
- - -
- -
- {steps.map((step) => ( -
- -

{step.title}

-

{step.body}

-
- ))} -
-
-
-
- ); -} diff --git a/packages/cli-old/template/vite-wv/src/index.css b/packages/cli-old/template/vite-wv/src/index.css deleted file mode 100644 index 6a1d0b1f..00000000 --- a/packages/cli-old/template/vite-wv/src/index.css +++ /dev/null @@ -1,96 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -:root { - --background: hsl(42 33% 98%); - --foreground: hsl(222 47% 11%); - --card: hsl(0 0% 100%); - --card-foreground: hsl(222 47% 11%); - --popover: hsl(0 0% 100%); - --popover-foreground: hsl(222 47% 11%); - --primary: hsl(197 82% 44%); - --primary-foreground: hsl(210 40% 98%); - --secondary: hsl(210 20% 93%); - --secondary-foreground: hsl(222 47% 11%); - --muted: hsl(42 21% 94%); - --muted-foreground: hsl(215 16% 40%); - --accent: hsl(32 88% 92%); - --accent-foreground: hsl(24 10% 10%); - --destructive: hsl(0 72% 51%); - --destructive-foreground: hsl(210 40% 98%); - --border: hsl(30 14% 86%); - --input: hsl(30 14% 86%); - --ring: hsl(197 82% 44%); - --radius: 1rem; -} - -.dark { - color-scheme: dark; - --background: hsl(221 39% 11%); - --foreground: hsl(44 23% 92%); - --card: hsl(222 33% 15%); - --card-foreground: hsl(44 23% 92%); - --popover: hsl(222 33% 15%); - --popover-foreground: hsl(44 23% 92%); - --primary: hsl(190 82% 62%); - --primary-foreground: hsl(222 47% 11%); - --secondary: hsl(219 19% 22%); - --secondary-foreground: hsl(44 23% 92%); - --muted: hsl(219 19% 22%); - --muted-foreground: hsl(215 20% 72%); - --accent: hsl(27 42% 28%); - --accent-foreground: hsl(44 23% 92%); - --destructive: hsl(0 63% 54%); - --destructive-foreground: hsl(210 40% 98%); - --border: hsl(219 19% 26%); - --input: hsl(219 19% 26%); - --ring: hsl(190 82% 62%); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); -} - -@layer base { - * { - @apply border-border; - } - - html { - color-scheme: light; - } - - body { - background-color: var(--background); - color: var(--foreground); - font-family: - "Instrument Sans", - Inter, - ui-sans-serif, - system-ui, - sans-serif; - min-width: 320px; - } -} diff --git a/packages/cli-old/template/vite-wv/src/lib/utils.ts b/packages/cli-old/template/vite-wv/src/lib/utils.ts deleted file mode 100644 index a5ef1935..00000000 --- a/packages/cli-old/template/vite-wv/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/cli-old/template/vite-wv/src/main.tsx b/packages/cli-old/template/vite-wv/src/main.tsx deleted file mode 100644 index f61aedf6..00000000 --- a/packages/cli-old/template/vite-wv/src/main.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { RouterProvider } from "@tanstack/react-router"; -import React from "react"; -import ReactDOM from "react-dom/client"; -import "./index.css"; -import { router } from "./router"; - -const queryClient = new QueryClient(); - -const rootElement = document.getElementById("root"); -if (!rootElement) { - throw new Error("Root element with id 'root' not found"); -} - -ReactDOM.createRoot(rootElement).render( - - - - - , -); diff --git a/packages/cli-old/template/vite-wv/src/router.tsx b/packages/cli-old/template/vite-wv/src/router.tsx deleted file mode 100644 index d21c9dbe..00000000 --- a/packages/cli-old/template/vite-wv/src/router.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { - Link, - Outlet, - createHashHistory, - createRootRoute, - createRoute, - createRouter, -} from "@tanstack/react-router"; -import App from "./App"; -import { QueryDemoPage } from "./routes/query-demo"; - -const rootRoute = createRootRoute({ - component: RootLayout, -}); - -const indexRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/", - component: App, -}); - -const queryDemoRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/query", - component: QueryDemoPage, -}); - -const routeTree = rootRoute.addChildren([indexRoute, queryDemoRoute]); - -export const router = createRouter({ - routeTree, - history: createHashHistory(), -}); - -declare module "@tanstack/react-router" { - interface Register { - router: typeof router; - } -} - -function RootLayout() { - return ( -
-
- -
- -
- ); -} diff --git a/packages/cli-old/template/vite-wv/src/routes/query-demo.tsx b/packages/cli-old/template/vite-wv/src/routes/query-demo.tsx deleted file mode 100644 index f1f8aff9..00000000 --- a/packages/cli-old/template/vite-wv/src/routes/query-demo.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { Link } from "@tanstack/react-router"; - -const getConnectionHint = async (): Promise => { - await new Promise((resolve) => setTimeout(resolve, 180)); - return "Use fmFetch or generated clients once your FileMaker file is ready."; -}; - -export function QueryDemoPage() { - const hintQuery = useQuery({ - queryKey: ["starter-connection-hint"], - queryFn: getConnectionHint, - }); - - return ( -
-
-

React Query ready

-

TanStack Query is preconfigured

-

- This route is rendered by TanStack Router using hash history, which is recommended for FileMaker Web Viewer - apps. -

- -
- {hintQuery.isLoading ? "Loading starter data..." : hintQuery.data} -
- -
- - Back to starter - -
-
-
- ); -} diff --git a/packages/cli-old/template/vite-wv/tsconfig.json b/packages/cli-old/template/vite-wv/tsconfig.json deleted file mode 100644 index 1565cf30..00000000 --- a/packages/cli-old/template/vite-wv/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "baseUrl": ".", - "noEmit": true, - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src"] -} diff --git a/packages/cli-old/template/vite-wv/vite.config.ts b/packages/cli-old/template/vite-wv/vite.config.ts deleted file mode 100644 index 8e23f082..00000000 --- a/packages/cli-old/template/vite-wv/vite.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import path from "node:path"; -import react from "@vitejs/plugin-react"; -import { fmBridge } from "@proofkit/webviewer/vite-plugins"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; -import { viteSingleFile } from "vite-plugin-singlefile"; - -export default defineConfig({ - server: { - port: 5175, - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, - plugins: [fmBridge(), react(), tailwindcss(), viteSingleFile()], -}); diff --git a/packages/cli-old/tests/browser-apps.smoke.test.ts b/packages/cli-old/tests/browser-apps.smoke.test.ts deleted file mode 100644 index 6ebf69e9..00000000 --- a/packages/cli-old/tests/browser-apps.smoke.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { z } from "zod/v4"; - -import { verifySmokeProjectBuilds } from "./test-utils"; - -const smokeEnvSchema = z.object({ - OTTO_SERVER_URL: z.url(), - OTTO_ADMIN_API_KEY: z.string().min(1), - FM_DATA_API_KEY: z.string().min(1), - FM_FILE_NAME: z.string().min(1), - FM_LAYOUT_NAME: z.string().min(1), -}); - -const parsedSmokeEnv = smokeEnvSchema.safeParse(process.env); -const describeWhenSmokeEnvPresent = parsedSmokeEnv.success ? describe : describe.skip; - -if (!parsedSmokeEnv.success) { - const missingKeys = [...new Set(parsedSmokeEnv.error.issues.map((issue) => issue.path.join(".")))]; - console.warn(`Skipping external integration smoke tests; missing required env vars: ${missingKeys.join(", ")}`); -} - -describeWhenSmokeEnvPresent("External integration smoke tests (non-interactive CLI)", () => { - if (!parsedSmokeEnv.success) { - return; - } - - // Use root-level tmp directory for test outputs - const testDir = join(__dirname, "..", "..", "tmp", "cli-tests"); - const cliPath = join(__dirname, "..", "dist", "index.js"); - const projectName = "test-fm-project"; - const projectDir = join(testDir, projectName); - - // Required for live Otto/FileMaker integration smoke coverage. - const testEnv = parsedSmokeEnv.data; - - beforeEach( - () => { - // Clean up any stale test project from previous runs - if (existsSync(projectDir)) { - rmSync(projectDir, { recursive: true, force: true }); - } - // Ensure the test directory exists - mkdirSync(testDir, { recursive: true }); - }, - 30_000, // 30s timeout for cleanup of large node_modules - ); - - it("should create a browser project with FileMaker integration in non-interactive mode", () => { - // Build the command with all necessary flags for non-interactive mode - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType browser", - "--dataSource filemaker", - `--server "${testEnv.OTTO_SERVER_URL}"`, - `--adminApiKey "${testEnv.OTTO_ADMIN_API_KEY}"`, - `--dataApiKey "${testEnv.FM_DATA_API_KEY}"`, - `--fileName "${testEnv.FM_FILE_NAME}"`, - `--layoutName "${testEnv.FM_LAYOUT_NAME}"`, - "--noGit", // Skip git initialization for testing - ].join(" "); - - // Execute the command - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - // Verify project structure - expect(existsSync(projectDir)).toBe(true); - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(projectDir, ".env"))).toBe(true); - - // Verify package.json content - const pkgJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")); - expect(pkgJson.name).toBe(projectName); - - // Verify proofkit.json content - const proofkitConfig = JSON.parse(readFileSync(join(projectDir, "proofkit.json"), "utf-8")); - expect(proofkitConfig.appType).toBe("browser"); - expect(proofkitConfig.dataSources).toContainEqual( - expect.objectContaining({ - type: "fm", - name: "filemaker", - }), - ); - - // Verify the project can be built successfully - verifySmokeProjectBuilds(projectDir); - }); -}); diff --git a/packages/cli-old/tests/cli.test.ts b/packages/cli-old/tests/cli.test.ts deleted file mode 100644 index f86d3fb4..00000000 --- a/packages/cli-old/tests/cli.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { execSync } from "node:child_process"; -import { describe, expect, it } from "vitest"; - -describe("CLI Basic Tests", () => { - it("should show help without throwing", () => { - expect(() => { - execSync("node ../dist/index.js --help", { - cwd: import.meta.dirname, - encoding: "utf-8", - }); - }).not.toThrow(); - }); - - it("should be executable", () => { - expect(() => { - execSync("node ../dist/index.js --version", { - cwd: import.meta.dirname, - encoding: "utf-8", - }); - }).not.toThrow(); - }); -}); diff --git a/packages/cli-old/tests/init-non-interactive-failures.test.ts b/packages/cli-old/tests/init-non-interactive-failures.test.ts deleted file mode 100644 index af855a83..00000000 --- a/packages/cli-old/tests/init-non-interactive-failures.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; - -type ExecFailure = Error & { - status?: number | null; - stdout?: string | Buffer; - stderr?: string | Buffer; -}; -const typegenCommandPattern = /\b(?:npm run|pnpm|yarn|bun)\s+typegen\b/; - -function toText(value: string | Buffer | undefined) { - if (typeof value === "string") { - return value; - } - if (!value) { - return ""; - } - return value.toString("utf-8"); -} - -describe("Init Non-Interactive Failure Paths", () => { - const cliRoot = join(__dirname, ".."); - const testDir = join(__dirname, "..", "..", "tmp", "init-failure-tests"); - const cliPath = join(__dirname, "..", "dist", "index.js"); - - beforeEach(() => { - rmSync(testDir, { recursive: true, force: true }); - mkdirSync(testDir, { recursive: true }); - }); - - const rebuildCli = () => { - execFileSync("pnpm", ["build"], { - cwd: cliRoot, - env: process.env, - stdio: "pipe", - }); - }; - - const runInitCommand = (args: string[], cwd = testDir) => { - const execute = () => - execFileSync("node", [cliPath, "init", ...args], { - cwd, - env: process.env, - stdio: "pipe", - encoding: "utf-8", - }); - - try { - return execute(); - } catch (error) { - const failure = error as ExecFailure; - const output = `${toText(failure.stdout)}\n${toText(failure.stderr)}`; - if (output.includes("Cannot find module") && output.includes("dist/index.js")) { - rebuildCli(); - return execute(); - } - throw error; - } - }; - - const runInitExpectFailure = (args: string[], cwd = testDir) => { - try { - runInitCommand(args, cwd); - throw new Error(`Expected init to fail, but it succeeded: ${args.join(" ")}`); - } catch (error) { - const failure = error as ExecFailure; - if (typeof failure.status === "number" || failure.status === null) { - return { - status: failure.status, - stdout: toText(failure.stdout), - stderr: toText(failure.stderr), - }; - } - throw error; - } - }; - - const runInitExpectSuccess = (args: string[], cwd = testDir) => runInitCommand(args, cwd); - - it("fails in non-interactive mode without a project name and does not scaffold", () => { - writeFileSync(join(testDir, "sentinel.txt"), "keep"); - - const result = runInitExpectFailure(["--non-interactive", "--appType", "webviewer", "--noInstall", "--noGit"]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Project name is required in non-interactive mode."); - expect(readdirSync(testDir).sort()).toEqual(["sentinel.txt"]); - }); - - it("fails fast for invalid non-interactive app names and does not create a project directory", () => { - const projectName = "Bad Name"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--appType", - "webviewer", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Name must consist of only lowercase alphanumeric characters, '-', and '_'"); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("fails for invalid scoped-path edge cases before mutating the target directory", () => { - writeFileSync(join(testDir, "README.md"), "existing content"); - - const result = runInitExpectFailure([ - "@scope", - "--non-interactive", - "--appType", - "webviewer", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Name must consist of only lowercase alphanumeric characters, '-', and '_'"); - expect(readFileSync(join(testDir, "README.md"), "utf-8")).toBe("existing content"); - expect(existsSync(join(testDir, "package.json"))).toBe(false); - expect(existsSync(join(testDir, "proofkit.json"))).toBe(false); - }); - - it("fails for partial FileMaker schema flags without creating a scaffold", () => { - const projectName = "partial-filemaker-flags"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--appType", - "webviewer", - "--dataSource", - "filemaker", - "--layoutName", - "Contacts", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Both --layoutName and --schemaName must be provided together."); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("fails when FileMaker flags are passed without selecting the filemaker data source", () => { - const projectName = "unsupported-filemaker-flags"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--appType", - "webviewer", - "--layoutName", - "Contacts", - "--schemaName", - "Contacts", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("FileMaker flags require --dataSource filemaker in non-interactive mode."); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("preserves existing directory contents when validation fails even with --force", () => { - const projectName = "force-validation-failure"; - const projectDir = join(testDir, projectName); - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, "README.md"), "existing content"); - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--appType", - "webviewer", - "--force", - "--layoutName", - "Contacts", - "--schemaName", - "Contacts", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("FileMaker flags require --dataSource filemaker in non-interactive mode."); - expect(readFileSync(join(projectDir, "README.md"), "utf-8")).toBe("existing content"); - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(false); - }); - - it("does not surface typegen guidance for browser scaffolds without a typegen script", () => { - const projectName = "browser-no-fm-guidance"; - const output = runInitExpectSuccess([ - projectName, - "--non-interactive", - "--appType", - "browser", - "--dataSource", - "none", - "--noInstall", - "--noGit", - ]); - - const packageJson = JSON.parse(readFileSync(join(testDir, projectName, "package.json"), "utf-8")) as { - scripts?: Record; - }; - expect(packageJson.scripts?.typegen).toBeUndefined(); - expect(output).not.toMatch(typegenCommandPattern); - }); -}); diff --git a/packages/cli-old/tests/init-post-init-generation-errors.test.ts b/packages/cli-old/tests/init-post-init-generation-errors.test.ts deleted file mode 100644 index f13e9dff..00000000 --- a/packages/cli-old/tests/init-post-init-generation-errors.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { createPostInitGenerationError, isMissingTypegenCommandError } from "~/cli/init"; - -vi.mock("@inquirer/prompts", () => ({ - checkbox: vi.fn(), - confirm: vi.fn(), - input: vi.fn(), - password: vi.fn(), - search: vi.fn(), - select: vi.fn(), -})); - -describe("init post-init generation error handling", () => { - it("detects missing typegen command failures", () => { - const commandError = new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ); - - expect(isMissingTypegenCommandError(commandError)).toBe(true); - }); - - it("does not classify broad pnpm typegen execution failures as missing command", () => { - const commandError = new Error( - "Command failed with exit code 1: pnpm typegen\nError: connect ECONNREFUSED 127.0.0.1:3000", - ); - - expect(isMissingTypegenCommandError(commandError)).toBe(false); - }); - - it("creates browser-specific guidance for missing typegen command failures", () => { - const commandError = new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ); - - const userFacingError = createPostInitGenerationError({ - error: commandError, - appType: "browser", - projectDir: "/tmp/demo-browser", - }); - - expect(userFacingError.message).toContain("Post-init generation failed after scaffolding."); - expect(userFacingError.message).toContain("Project created at: /tmp/demo-browser"); - expect(userFacingError.message).toContain("browser scaffolds do not define that script"); - expect(userFacingError.message).toContain("proofkit typegen"); - }); - - it("creates generic recovery guidance for other generation failures", () => { - const commandError = new Error("Unable to read layout metadata"); - - const userFacingError = createPostInitGenerationError({ - error: commandError, - appType: "webviewer", - projectDir: "/tmp/demo-webviewer", - }); - - expect(userFacingError.message).toContain("Post-init generation failed after scaffolding."); - expect(userFacingError.message).toContain("Project created at: /tmp/demo-webviewer"); - expect(userFacingError.message).toContain("Retry `proofkit typegen`"); - expect(userFacingError.message).toContain("Underlying error: Unable to read layout metadata"); - }); -}); diff --git a/packages/cli-old/tests/init-run-init-regression.test.ts b/packages/cli-old/tests/init-run-init-regression.test.ts deleted file mode 100644 index 6422d97b..00000000 --- a/packages/cli-old/tests/init-run-init-regression.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const { - createBareProjectMock, - setImportAliasMock, - promptForFileMakerDataSourceMock, - runCodegenCommandMock, - initializeGitMock, - logNextStepsMock, - readJSONSyncMock, - writeJSONSyncMock, - execaMock, - mockState, -} = vi.hoisted(() => ({ - createBareProjectMock: vi.fn(), - setImportAliasMock: vi.fn(), - promptForFileMakerDataSourceMock: vi.fn(), - runCodegenCommandMock: vi.fn(), - initializeGitMock: vi.fn(), - logNextStepsMock: vi.fn(), - readJSONSyncMock: vi.fn(), - writeJSONSyncMock: vi.fn(), - execaMock: vi.fn(), - mockState: { - appType: undefined as "browser" | "webviewer" | undefined, - ui: "shadcn" as "shadcn" | "mantine", - projectDir: "/tmp/proofkit-regression", - }, -})); - -vi.mock("@clack/prompts", () => ({ - intro: vi.fn(), - outro: vi.fn(), - note: vi.fn(), - cancel: vi.fn(), - log: { - error: vi.fn(), - info: vi.fn(), - message: vi.fn(), - step: vi.fn(), - success: vi.fn(), - warn: vi.fn(), - }, - spinner: vi.fn(() => ({ - message: vi.fn(), - start: vi.fn(), - stop: vi.fn(), - })), - isCancel: vi.fn(() => false), - select: vi.fn(), - text: vi.fn(), -})); - -vi.mock("@inquirer/prompts", () => ({ - checkbox: vi.fn(), - confirm: vi.fn(), - input: vi.fn(), - password: vi.fn(), - search: vi.fn(), - select: vi.fn(), -})); - -vi.mock("fs-extra", () => ({ - default: { - readJSONSync: readJSONSyncMock, - writeJSONSync: writeJSONSyncMock, - }, -})); - -vi.mock("execa", () => ({ - execa: execaMock, -})); - -vi.mock("~/helpers/createProject.js", () => ({ - createBareProject: createBareProjectMock, -})); - -vi.mock("~/helpers/setImportAlias.js", () => ({ - setImportAlias: setImportAliasMock, -})); - -vi.mock("~/cli/add/data-source/filemaker.js", () => ({ - promptForFileMakerDataSource: promptForFileMakerDataSourceMock, -})); - -vi.mock("~/generators/fmdapi.js", () => ({ - runCodegenCommand: runCodegenCommandMock, -})); - -vi.mock("~/helpers/git.js", () => ({ - initializeGit: initializeGitMock, -})); - -vi.mock("~/helpers/logNextSteps.js", () => ({ - logNextSteps: logNextStepsMock, -})); - -vi.mock("~/helpers/installDependencies.js", () => ({ - installDependencies: vi.fn(), -})); - -vi.mock("~/generators/auth.js", () => ({ - addAuth: vi.fn(), -})); - -vi.mock("~/installers/index.js", () => ({ - buildPkgInstallerMap: vi.fn(() => ({})), -})); - -vi.mock("~/state.js", () => ({ - state: mockState, - initProgramState: vi.fn(), - isNonInteractiveMode: vi.fn(() => true), -})); - -vi.mock("~/utils/getProofKitVersion.js", () => ({ - getVersion: vi.fn(() => "0.0.0-test"), -})); - -vi.mock("~/utils/getUserPkgManager.js", () => ({ - getUserPkgManager: vi.fn(() => "pnpm"), -})); - -vi.mock("~/utils/parseNameAndPath.js", () => ({ - parseNameAndPath: vi.fn((name: string) => [name, name]), -})); - -vi.mock("~/utils/parseSettings.js", () => ({ - setSettings: vi.fn(), -})); - -vi.mock("~/utils/validateAppName.js", () => ({ - validateAppName: vi.fn(() => undefined), -})); - -vi.mock("~/cli/utils.js", () => ({ - abortIfCancel: vi.fn((value: unknown) => value), -})); - -import { runInit } from "~/cli/init"; - -const browserFilemakerFlags = { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - server: undefined, - adminApiKey: undefined, - fileName: "", - layoutName: "", - schemaName: "", - dataApiKey: "", - fmServerURL: "", - auth: "none" as const, - dataSource: "filemaker" as const, - ui: "shadcn" as const, - CI: false, - nonInteractive: true, - tailwind: false, - trpc: false, - prisma: false, - drizzle: false, - appRouter: false, -}; - -describe("runInit browser post-init typegen regression", () => { - beforeEach(() => { - vi.clearAllMocks(); - - mockState.appType = undefined; - mockState.ui = "shadcn"; - mockState.projectDir = "/tmp/proofkit-regression"; - - createBareProjectMock.mockResolvedValue("/tmp/proofkit-regression/demo-browser"); - readJSONSyncMock.mockReturnValue({ name: "placeholder-app" }); - execaMock.mockResolvedValue({ stdout: "9.0.0" }); - promptForFileMakerDataSourceMock.mockResolvedValue(undefined); - - runCodegenCommandMock.mockRejectedValue( - new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ), - ); - }); - - it("does not run initial codegen for browser scaffolds after filemaker setup", async () => { - await expect(runInit("demo-browser", browserFilemakerFlags)).resolves.toBeUndefined(); - - expect(promptForFileMakerDataSourceMock).toHaveBeenCalledWith( - expect.objectContaining({ - projectDir: "/tmp/proofkit-regression/demo-browser", - }), - ); - expect(runCodegenCommandMock).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/cli-old/tests/init-scaffold-contract.test.ts b/packages/cli-old/tests/init-scaffold-contract.test.ts deleted file mode 100644 index 8d8a7332..00000000 --- a/packages/cli-old/tests/init-scaffold-contract.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { parse as parseJsonc } from "jsonc-parser"; -import { beforeEach, describe, expect, it } from "vitest"; - -interface PackageJsonShape { - version?: string; - name?: string; - packageManager?: string; - scripts?: Record; - dependencies?: Record; - devDependencies?: Record; - proofkitMetadata?: { - initVersion?: string; - }; -} - -interface ProofkitSettings { - appType?: string; - ui?: string; - envFile?: string; - dataSources?: unknown[]; -} - -const cliPath = join(__dirname, "..", "dist", "index.js"); -const testDir = join(__dirname, "..", "..", "tmp", "cli-contract-tests"); -const browserProjectName = "contract-browser-project"; -const webviewerProjectName = "contract-webviewer-project"; -const browserProjectDir = join(testDir, browserProjectName); -const webviewerProjectDir = join(testDir, webviewerProjectName); -const cliPackageJsonPath = join(__dirname, "..", "package.json"); -const cliPackageJson = readJsonFile(cliPackageJsonPath); -const cliVersion = cliPackageJson.version ?? ""; -const expectedProofkitTag = cliVersion.includes("-") ? "beta" : "latest"; -const packageManagerPattern = /^(npm|pnpm|yarn|bun)@/; -const ansiStylePrefixPattern = /^[0-9;]*m/; - -function runInit({ appType, projectName }: { appType: "browser" | "webviewer"; projectName: string }): string { - return execFileSync( - "node", - [ - cliPath, - "init", - projectName, - "--non-interactive", - "--appType", - appType, - "--dataSource", - "none", - "--noGit", - "--noInstall", - ], - { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }, - ); -} - -function readJsonFile(filePath: string): T { - return JSON.parse(readFileSync(filePath, "utf-8")) as T; -} - -function getProofkitDependencyVersions(pkg: PackageJsonShape): string[] { - const combined = { - ...(pkg.dependencies ?? {}), - ...(pkg.devDependencies ?? {}), - }; - - return Object.entries(combined) - .filter(([name]) => name.startsWith("@proofkit/")) - .map(([, version]) => version); -} - -function allProofkitDependenciesUseCurrentReleaseTag(pkg: PackageJsonShape): boolean { - const versions = getProofkitDependencyVersions(pkg); - return versions.length > 0 && versions.every((version) => version === expectedProofkitTag); -} - -function checkNodeSyntax(projectDir: string, relativeFilePath: string): boolean { - try { - execFileSync("node", ["--check", relativeFilePath], { - cwd: projectDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - - return true; - } catch { - return false; - } -} - -function getPackageManagerName(packageJson: PackageJsonShape): "npm" | "pnpm" | "yarn" | "bun" { - const raw = packageJson.packageManager?.split("@")[0]; - if (raw === "pnpm" || raw === "yarn" || raw === "bun") { - return raw; - } - return "npm"; -} - -function formatRunCommand(pkgManager: "npm" | "pnpm" | "yarn" | "bun", command: string): string { - return pkgManager === "npm" || pkgManager === "bun" ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`; -} - -function sanitizeOutput(output: string): string { - return output - .split("\u001b[") - .map((segment, index) => (index === 0 ? segment : segment.replace(ansiStylePrefixPattern, ""))) - .join(""); -} - -function outputSuggestsCommand(output: string, command: string): boolean { - return output.includes(` ${command}`); -} - -describe("Init scaffold contract tests", () => { - beforeEach(() => { - rmSync(testDir, { recursive: true, force: true }); - mkdirSync(testDir, { recursive: true }); - }); - - it("creates deterministic browser scaffold output in non-interactive mode", () => { - const initOutput = runInit({ - appType: "browser", - projectName: browserProjectName, - }); - const normalizedOutput = sanitizeOutput(initOutput); - - expect(existsSync(browserProjectDir)).toBe(true); - expect(existsSync(join(browserProjectDir, "package.json"))).toBe(true); - expect(existsSync(join(browserProjectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(browserProjectDir, ".env"))).toBe(true); - expect(existsSync(join(browserProjectDir, "src", "lib", "env.ts"))).toBe(true); - expect(existsSync(join(browserProjectDir, "src", "app", "layout.tsx"))).toBe(true); - expect(existsSync(join(browserProjectDir, "postcss.config.mjs"))).toBe(true); - - const packageJson = readJsonFile(join(browserProjectDir, "package.json")); - expect(packageJson.name).toBe(browserProjectName); - expect(packageJson.scripts?.dev).toBe("next dev --turbopack"); - expect(packageJson.scripts?.build).toBe("next build --turbopack"); - expect(packageJson.scripts?.proofkit).toBe("proofkit"); - expect(packageJson.proofkitMetadata?.initVersion).toBe(cliVersion); - expect(packageJson.packageManager).toMatch(packageManagerPattern); - expect(allProofkitDependenciesUseCurrentReleaseTag(packageJson)).toBe(true); - const pkgManager = getPackageManagerName(packageJson); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "typegen"))).toBe(false); - - const proofkitConfig = readJsonFile(join(browserProjectDir, "proofkit.json")); - expect(proofkitConfig.appType).toBe("browser"); - expect(proofkitConfig.ui).toBe("shadcn"); - expect(proofkitConfig.envFile).toBe(".env"); - expect(proofkitConfig.dataSources).toEqual([]); - - // Compile-equivalent smoke check without external installs. - expect(checkNodeSyntax(browserProjectDir, "postcss.config.mjs")).toBe(true); - }); - - it("creates deterministic webviewer scaffold output in non-interactive mode", () => { - const initOutput = runInit({ - appType: "webviewer", - projectName: webviewerProjectName, - }); - const normalizedOutput = sanitizeOutput(initOutput); - - expect(existsSync(webviewerProjectDir)).toBe(true); - expect(existsSync(join(webviewerProjectDir, "package.json"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "proofkit-typegen.config.jsonc"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, ".env"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "src", "main.tsx"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "scripts", "launch-fm.js"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "scripts", "upload.js"))).toBe(true); - - const packageJson = readJsonFile(join(webviewerProjectDir, "package.json")); - expect(packageJson.name).toBe(webviewerProjectName); - expect(packageJson.scripts?.build).toBe("vite build"); - expect(packageJson.scripts?.typegen).toBe("typegen"); - expect(packageJson.scripts?.["typegen:ui"]).toBe("typegen ui"); - expect(packageJson.scripts?.proofkit).toBe("proofkit"); - expect(packageJson.proofkitMetadata?.initVersion).toBe(cliVersion); - expect(packageJson.packageManager).toMatch(packageManagerPattern); - expect(allProofkitDependenciesUseCurrentReleaseTag(packageJson)).toBe(true); - const pkgManager = getPackageManagerName(packageJson); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "typegen"))).toBe(true); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "launch-fm"))).toBe(true); - - const proofkitConfig = readJsonFile(join(webviewerProjectDir, "proofkit.json")); - expect(proofkitConfig.appType).toBe("webviewer"); - expect(proofkitConfig.ui).toBe("shadcn"); - expect(proofkitConfig.envFile).toBe(".env"); - expect(proofkitConfig.dataSources).toEqual([]); - - const typegenConfigText = readFileSync(join(webviewerProjectDir, "proofkit-typegen.config.jsonc"), "utf-8"); - const typegenConfig = parseJsonc(typegenConfigText) as { - config?: { - type?: string; - path?: string; - validator?: string; - webviewerScriptName?: string; - fmMcp?: { - enabled?: boolean; - }; - }; - }; - expect(typegenConfig.config?.type).toBe("fmdapi"); - expect(typegenConfig.config?.path).toBe("./src/config/schemas/filemaker"); - expect(typegenConfig.config?.validator).toBe("zod/v4"); - expect(typegenConfig.config?.webviewerScriptName).toBe("ExecuteDataApi"); - expect(typegenConfig.config?.fmMcp?.enabled).toBe(true); - - // Compile-equivalent smoke checks without external installs. - expect(checkNodeSyntax(webviewerProjectDir, "scripts/launch-fm.js")).toBe(true); - expect(checkNodeSyntax(webviewerProjectDir, "scripts/upload.js")).toBe(true); - }); -}); diff --git a/packages/cli-old/tests/setup.ts b/packages/cli-old/tests/setup.ts deleted file mode 100644 index 9f7ba3cd..00000000 --- a/packages/cli-old/tests/setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { execSync } from "node:child_process"; -import path, { join } from "node:path"; -import dotenv from "dotenv"; -import { beforeAll } from "vitest"; - -beforeAll(() => { - // Ensure test environment variables are loaded - dotenv.config({ path: path.resolve(__dirname, "../.env.test") }); - process.env.PROOFKIT_SKIP_VERSION_CHECK = "1"; -}); - -// Build the CLI before running any tests -execSync("pnpm build", { cwd: join(__dirname, "..") }); diff --git a/packages/cli-old/tests/test-utils.ts b/packages/cli-old/tests/test-utils.ts deleted file mode 100644 index 7c8ce0b9..00000000 --- a/packages/cli-old/tests/test-utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { execSync } from "node:child_process"; -import { readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -/** - * Smoke-test helper only: swap workspace refs to published tags so install/build - * validates what end users can actually fetch from the registry. - */ -function usePublishedProofkitVersionsForSmoke(projectDir: string): void { - const pkgPath = join(projectDir, "package.json"); - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - - const replaceProofkitVersions = (deps: Record | undefined) => { - if (!deps) { - return; - } - for (const name of Object.keys(deps)) { - if (name.startsWith("@proofkit/")) { - console.log(` Replacing ${name}@${deps[name]} with latest`); - deps[name] = "latest"; - } - } - }; - - console.log("Using latest published @proofkit/* versions..."); - replaceProofkitVersions(pkg.dependencies); - replaceProofkitVersions(pkg.devDependencies); - - writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); -} - -/** - * Verifies that a project at the given directory can be built without errors - * @param projectDir The directory containing the project to build - * @throws If the build fails - */ -export function verifySmokeProjectBuilds(projectDir: string): void { - console.log(`\nVerifying project build in ${projectDir}...`); - - try { - // Smoke tests intentionally validate published package installability. - usePublishedProofkitVersionsForSmoke(projectDir); - - console.log("Installing dependencies..."); - // Run pnpm install while ignoring workspace settings - execSync("pnpm install --prefer-offline --ignore-workspace", { - cwd: projectDir, - stdio: "inherit", - encoding: "utf-8", - env: { - ...process.env, - PNPM_DEBUG: "1", // Enable debug logging - }, - }); - - console.log("Building project..."); - execSync("pnpm build", { - cwd: projectDir, - stdio: "inherit", - encoding: "utf-8", - env: { - ...process.env, - NEXT_TELEMETRY_DISABLED: "1", - }, - }); - } catch (error) { - console.error("Build process failed:", error); - throw error; - } -} diff --git a/packages/cli-old/tests/webviewer-apps.test.ts b/packages/cli-old/tests/webviewer-apps.test.ts deleted file mode 100644 index 0f757955..00000000 --- a/packages/cli-old/tests/webviewer-apps.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; - -const nonInteractiveDirectoryError = /already exists and isn't empty/; - -describe("Web Viewer CLI Tests", () => { - const testDir = join(__dirname, "..", "..", "tmp", "cli-tests"); - const cliPath = join(__dirname, "..", "dist", "index.js"); - const projectName = "test-webviewer-project"; - const projectDir = join(testDir, projectName); - - beforeEach(() => { - if (existsSync(projectDir)) { - rmSync(projectDir, { recursive: true, force: true }); - } - mkdirSync(testDir, { recursive: true }); - }); - - it("should create a webviewer project without FileMaker server setup", () => { - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(projectDir)).toBe(true); - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit-typegen.config.jsonc"))).toBe(true); - - const packageJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")); - expect(packageJson.scripts.typegen).toBe("typegen"); - expect(packageJson.scripts["typegen:ui"]).toBe("typegen ui"); - expect(packageJson.devDependencies["@proofkit/typegen"]).toBe("beta"); - - const proofkitConfig = JSON.parse(readFileSync(join(projectDir, "proofkit.json"), "utf-8")); - expect(proofkitConfig.appType).toBe("webviewer"); - expect(proofkitConfig.dataSources).toEqual([]); - }); - - it("should allow agent-only folders in non-interactive mode", () => { - mkdirSync(projectDir, { recursive: true }); - mkdirSync(join(projectDir, ".cursor"), { recursive: true }); - writeFileSync(join(projectDir, ".cursor", "rules.mdc"), "placeholder"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, ".cursor"))).toBe(true); - expect(existsSync(join(projectDir, ".cursor", "rules"))).toBe(false); - }); - - it("should allow hidden files in non-interactive mode", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, ".DS_Store"), "placeholder"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, ".DS_Store"))).toBe(true); - }); - - it("should fail in non-interactive mode when .gitignore already exists", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, ".gitignore"), "node_modules/\n"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(nonInteractiveDirectoryError); - - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - }); - - it("should fail without prompting when a non-interactive target directory has real files", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, "README.md"), "existing content"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(nonInteractiveDirectoryError); - - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - }); -}); diff --git a/packages/cli-old/tsconfig.json b/packages/cli-old/tsconfig.json deleted file mode 100644 index 5e73ded4..00000000 --- a/packages/cli-old/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": "./", - "paths": { - "~/*": ["./src/*"], - "@config/*": ["../config/*"] - }, - "checkJs": true, - "strictNullChecks": true - }, - "exclude": ["template"], - "include": ["src", "tsdown.config.ts", "../reset.d.ts", "index.d.ts"] -} diff --git a/packages/cli-old/tsdown.config.ts b/packages/cli-old/tsdown.config.ts deleted file mode 100644 index 6813e73c..00000000 --- a/packages/cli-old/tsdown.config.ts +++ /dev/null @@ -1,54 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import replacePlugin from "@rollup/plugin-replace"; -import fsExtra from "fs-extra"; -import { defineConfig } from "tsdown"; - -const replace = replacePlugin.default ?? replacePlugin; - -const { readJSONSync } = fsExtra; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const isDev = process.env.npm_lifecycle_event === "dev"; - -// Read package versions at build time -const readPackageVersion = (packagePath: string) => { - const packageJsonPath = path.join(__dirname, "..", packagePath, "package.json"); - const packageJson = readJSONSync(packageJsonPath); - if (!packageJson.version) { - throw new Error(`No version found in ${packageJsonPath}`); - } - return packageJson.version; -}; - -const FMDAPI_VERSION = readPackageVersion("fmdapi"); -const BETTER_AUTH_VERSION = readPackageVersion("better-auth"); -const WEBVIEWER_VERSION = readPackageVersion("webviewer"); -const TYPEGEN_VERSION = readPackageVersion("typegen"); - -export default defineConfig({ - clean: true, - entry: ["src/index.ts"], - format: ["esm"], - minify: !isDev, - target: "esnext", - outDir: "dist", - // Bundle workspace dependencies that shouldn't be external - noExternal: ["@proofkit/registry"], - // Keep Node.js built-in module imports as-is for better compatibility - nodeProtocol: false, - // Inject package versions and registry URL at build time - plugins: [ - replace({ - preventAssignment: true, - values: { - __FMDAPI_VERSION__: JSON.stringify(FMDAPI_VERSION), - __BETTER_AUTH_VERSION__: JSON.stringify(BETTER_AUTH_VERSION), - __WEBVIEWER_VERSION__: JSON.stringify(WEBVIEWER_VERSION), - __TYPEGEN_VERSION__: JSON.stringify(TYPEGEN_VERSION), - __REGISTRY_URL__: JSON.stringify(isDev ? "https://proofkit.localhost:1355" : "https://proofkit.proof.sh"), - }, - }), - ], - onSuccess: isDev ? "node dist/index.js" : undefined, -}); diff --git a/packages/cli-old/vitest.config.ts b/packages/cli-old/vitest.config.ts deleted file mode 100644 index c64f5913..00000000 --- a/packages/cli-old/vitest.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from "node:path"; -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(__dirname, "src"), - }, - }, - test: { - globals: true, - environment: "node", - setupFiles: ["./tests/setup.ts"], - include: ["tests/**/*.test.ts"], - // Deterministic contract/default tests only. - exclude: ["**/node_modules/**", "**/dist/**", "tests/**/*.smoke.test.ts"], - testTimeout: 60_000, // 60 seconds for CLI tests which can be slow - coverage: { - provider: "v8", - reporter: ["text", "json", "html"], - include: ["src/**/*.ts"], - }, - }, -}); diff --git a/packages/cli-old/vitest.smoke.config.ts b/packages/cli-old/vitest.smoke.config.ts deleted file mode 100644 index dae55ef3..00000000 --- a/packages/cli-old/vitest.smoke.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import path from "node:path"; -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(__dirname, "src"), - }, - }, - test: { - globals: true, - environment: "node", - setupFiles: ["./tests/setup.ts"], - include: ["tests/**/*.smoke.test.ts"], - exclude: ["**/node_modules/**", "**/dist/**"], - testTimeout: 60_000, - }, -}); From e7dc075e4bbe176346da66baf162896fe5b36c6c Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:14:47 -0500 Subject: [PATCH 2/3] simplify CLI to init/doctor/typegen; remove Commander MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - delete add/remove/deploy/upgrade/prompt cmds + orphaned installers/generators/helpers - typegen now thin alias to @proofkit/typegen (new ./cli export, runCli) — no duplicated codegen logic - drop commander dep; prune 43 unused devDeps - WV add-on installed during init; stale 'add addon' guidance now points to docs Co-Authored-By: Claude Opus 4.8 --- .../simplify-cli-init-doctor-typegen.md | 12 + apps/docs/content/docs/webviewer/package.mdx | 7 +- packages/cli/package.json | 55 +- packages/cli/src/cli/add/addon.ts | 57 - packages/cli/src/cli/add/auth.ts | 109 - .../cli/add/data-source/deploy-demo-file.ts | 96 - .../cli/src/cli/add/data-source/filemaker.ts | 437 ---- packages/cli/src/cli/add/data-source/index.ts | 45 - packages/cli/src/cli/add/fmschema.ts | 214 -- packages/cli/src/cli/add/index.ts | 12 - packages/cli/src/cli/add/page/index.ts | 231 -- .../add/page/post-install/table-infinite.ts | 12 - .../src/cli/add/page/post-install/table.ts | 123 -- packages/cli/src/cli/add/page/templates.ts | 85 - packages/cli/src/cli/add/page/types.ts | 19 - packages/cli/src/cli/deploy/index.ts | 489 ----- packages/cli/src/cli/init.ts | 479 ----- packages/cli/src/cli/menu.ts | 96 - packages/cli/src/cli/react-email.ts | 27 - packages/cli/src/cli/remove/data-source.ts | 152 -- packages/cli/src/cli/remove/index.ts | 71 - packages/cli/src/cli/remove/page.ts | 218 -- packages/cli/src/cli/remove/schema.ts | 100 - packages/cli/src/cli/tanstack-query.ts | 19 - packages/cli/src/cli/typegen/index.ts | 44 +- packages/cli/src/cli/update/index.ts | 28 - .../cli/src/cli/update/makeUpgradeCommand.ts | 23 - packages/cli/src/core/prompt.ts | 17 - packages/cli/src/generators/auth.ts | 83 - packages/cli/src/generators/fmdapi.ts | 525 ----- packages/cli/src/generators/route.ts | 40 - packages/cli/src/generators/tanstack-query.ts | 97 - packages/cli/src/globalOptions.ts | 7 - packages/cli/src/helpers/createProject.ts | 112 - packages/cli/src/helpers/fmMcp.ts | 56 - packages/cli/src/helpers/git.ts | 140 -- .../cli/src/helpers/installDependencies.ts | 242 --- packages/cli/src/helpers/installPackages.ts | 25 - packages/cli/src/helpers/logNextSteps.ts | 44 - packages/cli/src/helpers/replaceText.ts | 17 - packages/cli/src/helpers/scaffoldProject.ts | 132 -- packages/cli/src/helpers/selectBoilerplate.ts | 32 - packages/cli/src/helpers/setImportAlias.ts | 12 - packages/cli/src/helpers/stealth-init.ts | 20 - packages/cli/src/helpers/version-fetcher.ts | 131 -- packages/cli/src/index.ts | 289 +-- packages/cli/src/installers/auth-shared.ts | 49 - packages/cli/src/installers/better-auth.ts | 3 - packages/cli/src/installers/clerk.ts | 153 -- .../src/installers/dependencyVersionMap.ts | 116 - packages/cli/src/installers/envVars.ts | 43 - packages/cli/src/installers/index.ts | 31 - .../cli/src/installers/install-fm-addon.ts | 19 +- packages/cli/src/installers/nextAuth.ts | 189 -- packages/cli/src/installers/proofkit-auth.ts | 219 -- .../cli/src/installers/proofkit-webviewer.ts | 18 +- packages/cli/src/installers/react-email.ts | 209 -- packages/cli/src/upgrades/cursorRules.ts | 41 - packages/cli/src/upgrades/index.ts | 69 - packages/cli/src/upgrades/shadcn.ts | 55 - .../cli/src/utils/addPackageDependency.ts | 32 - packages/cli/src/utils/addToEnvs.ts | 131 -- packages/cli/src/utils/formatting.ts | 24 - packages/cli/src/utils/getUserPkgManager.ts | 21 - packages/cli/src/utils/isTTYError.ts | 1 - packages/cli/src/utils/logger.ts | 19 - .../cli/src/utils/proofkitReleaseChannel.ts | 93 - .../cli/src/utils/renderVersionWarning.ts | 86 - packages/cli/src/utils/ts-morph.ts | 25 - packages/cli/src/utils/validateImportAlias.ts | 6 - packages/cli/tests/cli.test.ts | 74 +- packages/cli/tests/default-command.test.ts | 2 +- packages/cli/tests/doctor.test.ts | 21 +- .../init-post-init-generation-errors.test.ts | 62 - .../tests/init-run-init-regression.test.ts | 240 --- packages/cli/tests/install-fm-addon.test.ts | 10 +- packages/typegen/package.json | 6 + packages/typegen/src/cli.ts | 35 +- pnpm-lock.yaml | 1901 +---------------- 79 files changed, 247 insertions(+), 9037 deletions(-) create mode 100644 .changeset/simplify-cli-init-doctor-typegen.md delete mode 100644 packages/cli/src/cli/add/addon.ts delete mode 100644 packages/cli/src/cli/add/auth.ts delete mode 100644 packages/cli/src/cli/add/data-source/deploy-demo-file.ts delete mode 100644 packages/cli/src/cli/add/data-source/filemaker.ts delete mode 100644 packages/cli/src/cli/add/data-source/index.ts delete mode 100644 packages/cli/src/cli/add/fmschema.ts delete mode 100644 packages/cli/src/cli/add/index.ts delete mode 100644 packages/cli/src/cli/add/page/index.ts delete mode 100644 packages/cli/src/cli/add/page/post-install/table-infinite.ts delete mode 100644 packages/cli/src/cli/add/page/post-install/table.ts delete mode 100644 packages/cli/src/cli/add/page/templates.ts delete mode 100644 packages/cli/src/cli/add/page/types.ts delete mode 100644 packages/cli/src/cli/deploy/index.ts delete mode 100644 packages/cli/src/cli/init.ts delete mode 100644 packages/cli/src/cli/menu.ts delete mode 100644 packages/cli/src/cli/react-email.ts delete mode 100644 packages/cli/src/cli/remove/data-source.ts delete mode 100644 packages/cli/src/cli/remove/index.ts delete mode 100644 packages/cli/src/cli/remove/page.ts delete mode 100644 packages/cli/src/cli/remove/schema.ts delete mode 100644 packages/cli/src/cli/tanstack-query.ts delete mode 100644 packages/cli/src/cli/update/index.ts delete mode 100644 packages/cli/src/cli/update/makeUpgradeCommand.ts delete mode 100644 packages/cli/src/core/prompt.ts delete mode 100644 packages/cli/src/generators/auth.ts delete mode 100644 packages/cli/src/generators/fmdapi.ts delete mode 100644 packages/cli/src/generators/route.ts delete mode 100644 packages/cli/src/generators/tanstack-query.ts delete mode 100644 packages/cli/src/globalOptions.ts delete mode 100644 packages/cli/src/helpers/createProject.ts delete mode 100644 packages/cli/src/helpers/fmMcp.ts delete mode 100644 packages/cli/src/helpers/git.ts delete mode 100644 packages/cli/src/helpers/installDependencies.ts delete mode 100644 packages/cli/src/helpers/installPackages.ts delete mode 100644 packages/cli/src/helpers/logNextSteps.ts delete mode 100644 packages/cli/src/helpers/replaceText.ts delete mode 100644 packages/cli/src/helpers/scaffoldProject.ts delete mode 100644 packages/cli/src/helpers/selectBoilerplate.ts delete mode 100644 packages/cli/src/helpers/setImportAlias.ts delete mode 100644 packages/cli/src/helpers/stealth-init.ts delete mode 100644 packages/cli/src/helpers/version-fetcher.ts delete mode 100644 packages/cli/src/installers/auth-shared.ts delete mode 100644 packages/cli/src/installers/better-auth.ts delete mode 100644 packages/cli/src/installers/clerk.ts delete mode 100644 packages/cli/src/installers/dependencyVersionMap.ts delete mode 100644 packages/cli/src/installers/envVars.ts delete mode 100644 packages/cli/src/installers/index.ts delete mode 100644 packages/cli/src/installers/nextAuth.ts delete mode 100644 packages/cli/src/installers/proofkit-auth.ts delete mode 100644 packages/cli/src/installers/react-email.ts delete mode 100644 packages/cli/src/upgrades/cursorRules.ts delete mode 100644 packages/cli/src/upgrades/index.ts delete mode 100644 packages/cli/src/upgrades/shadcn.ts delete mode 100644 packages/cli/src/utils/addPackageDependency.ts delete mode 100644 packages/cli/src/utils/addToEnvs.ts delete mode 100644 packages/cli/src/utils/formatting.ts delete mode 100644 packages/cli/src/utils/getUserPkgManager.ts delete mode 100644 packages/cli/src/utils/isTTYError.ts delete mode 100644 packages/cli/src/utils/logger.ts delete mode 100644 packages/cli/src/utils/proofkitReleaseChannel.ts delete mode 100644 packages/cli/src/utils/renderVersionWarning.ts delete mode 100644 packages/cli/src/utils/ts-morph.ts delete mode 100644 packages/cli/src/utils/validateImportAlias.ts delete mode 100644 packages/cli/tests/init-post-init-generation-errors.test.ts delete mode 100644 packages/cli/tests/init-run-init-regression.test.ts diff --git a/.changeset/simplify-cli-init-doctor-typegen.md b/.changeset/simplify-cli-init-doctor-typegen.md new file mode 100644 index 00000000..3e43f632 --- /dev/null +++ b/.changeset/simplify-cli-init-doctor-typegen.md @@ -0,0 +1,12 @@ +--- +"@proofkit/cli": minor +"@proofkit/typegen": minor +--- + +Simplify the ProofKit CLI to `init`, `doctor`, and `typegen`. + +- Remove the `add`, `remove`, `deploy`, `upgrade`, and `prompt` subcommands and their supporting installers/generators/helpers. The Web Viewer add-on is now downloaded and opened during `proofkit init`. +- `proofkit typegen` is now a thin alias that delegates entirely to `@proofkit/typegen` with no duplicated generation logic. Supports `--config`, `--env-path`, `--proofkit-token`, and `--reset-overrides`. +- Drop the Commander dependency; the CLI is now built entirely on `@effect/cli`. + +`@proofkit/typegen` now exposes its CLI runner through a new `@proofkit/typegen/cli` entrypoint (`runCli`). diff --git a/apps/docs/content/docs/webviewer/package.mdx b/apps/docs/content/docs/webviewer/package.mdx index 3f8723a5..68602fad 100644 --- a/apps/docs/content/docs/webviewer/package.mdx +++ b/apps/docs/content/docs/webviewer/package.mdx @@ -30,7 +30,7 @@ For web-based applications where you're looking to interact with the Data API us The [ProofKit AI workflow](/docs/ai/build-a-webviewer-app) can scaffold a full Web Viewer project and install the FileMaker add-on that provides the necessary layouts, scripts, and custom functions. Use that path when you want ProofKit to create the project structure for you. -If you already have a ProofKit Web Viewer project and need to install or update the FileMaker add-on manually, run `proofkit add addon webviewer` from the project root. That downloads the latest add-on from the ProofKit CDN and opens it in FileMaker; you still need to add the add-on into your FileMaker file. +When you scaffold a Web Viewer project with `proofkit init`, ProofKit downloads the latest FileMaker add-on from the ProofKit CDN and opens it in FileMaker automatically; you still need to add the add-on into your FileMaker file. For manual installation, follow the steps below. @@ -38,9 +38,8 @@ If you already have a ProofKit Web Viewer project and need to install or update {" "} This demo file is a very simplified example. To see more features, use the - [ProofKit AI workflow](/docs/ai/build-a-webviewer-app) to build a new app or run - `proofkit add addon webviewer` in an existing ProofKit project, then install - the FileMaker add-on. + [ProofKit AI workflow](/docs/ai/build-a-webviewer-app) to build a new app or + scaffold one with `proofkit init`, then install the FileMaker add-on. Use your preferred package manager to install the package. diff --git a/packages/cli/package.json b/packages/cli/package.json index dfc38be8..93bac81a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -60,8 +60,6 @@ "test:smoke": "PROOFKIT_RUN_SMOKE_TESTS=1 vitest run --config vitest.smoke.config.ts" }, "devDependencies": { - "@better-fetch/fetch": "1.1.17", - "@clack/core": "^0.3.5", "@clack/prompts": "^0.11.0", "@effect/cli": "0.74.0", "@effect/platform": "0.95.0", @@ -69,66 +67,25 @@ "@effect/printer": "0.48.0", "@effect/printer-ansi": "0.48.0", "@inquirer/prompts": "^8.3.2", - "@proofkit/better-auth": "workspace:*", "@proofkit/fmdapi": "workspace:*", "@proofkit/typegen": "workspace:*", - "@proofkit/webviewer": "workspace:*", - "@types/glob": "^8.1.0", + "@types/fs-extra": "^11.0.4", + "@types/gradient-string": "^1.1.6", + "@types/node": "^22.19.5", + "@types/randomstring": "^1.3.0", "axios": "^1.13.2", "chalk": "5.4.1", - "commander": "^14.0.2", "dotenv": "^16.6.1", "effect": "^3.20.0", - "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", - "ts-morph": "^26.0.0", - "type-fest": "^3.13.1", - "@auth/drizzle-adapter": "^1.11.1", - "@auth/prisma-adapter": "^1.6.0", - "@libsql/client": "^0.6.2", - "@planetscale/database": "^1.19.0", - "@prisma/adapter-planetscale": "^5.22.0", - "@prisma/client": "^5.22.0", - "@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", + "randomstring": "^1.3.1", "tsdown": "^0.14.2", + "type-fest": "^3.13.1", "typescript": "^5.9.3", "vitest": "^4.0.17", "zod": "^4.3.5" diff --git a/packages/cli/src/cli/add/addon.ts b/packages/cli/src/cli/add/addon.ts deleted file mode 100644 index 0eff9038..00000000 --- a/packages/cli/src/cli/add/addon.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Command } from "commander"; -import { select } from "~/cli/prompts.js"; -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { installFmAddonExplicitly } from "~/installers/install-fm-addon.js"; -import { initProgramState, isNonInteractiveMode } from "~/state.js"; -import { abortIfCancel } from "../utils.js"; - -type AddonTarget = "webviewer" | "auth"; - -async function resolveAddonTarget(name?: string): Promise { - if (name === "webviewer" || name === "auth") { - return name; - } - - if (isNonInteractiveMode()) { - throw new Error("Addon target is required in non-interactive mode. Use `proofkit add addon webviewer`."); - } - - return abortIfCancel( - await select({ - message: "Which add-on do you want to install locally?", - options: [ - { - value: "webviewer", - label: "Web Viewer", - hint: "ProofKit Web Viewer add-on", - }, - { value: "auth", label: "Auth", hint: "ProofKit Auth add-on" }, - ], - }), - ) as AddonTarget; -} - -export async function runAddAddonAction(targetName?: string) { - const target = await resolveAddonTarget(targetName); - - await installFmAddonExplicitly({ - addonName: target === "webviewer" ? "wv" : "auth", - }); -} - -export const makeAddAddonCommand = () => { - const addAddonCommand = new Command("addon") - .description("Install or update local FileMaker add-on files") - .argument("[target]", "Add-on to install locally (webviewer or auth)") - .addOption(nonInteractiveOption) - .addOption(debugOption) - .action(async (target) => { - await runAddAddonAction(target); - }); - - addAddonCommand.hook("preAction", (thisCommand) => { - initProgramState(thisCommand.opts()); - }); - - return addAddonCommand; -}; diff --git a/packages/cli/src/cli/add/auth.ts b/packages/cli/src/cli/add/auth.ts deleted file mode 100644 index a6c7e911..00000000 --- a/packages/cli/src/cli/add/auth.ts +++ /dev/null @@ -1,109 +0,0 @@ -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 { 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(nonInteractiveOption) - .addOption(debugOption) - - .action(async () => { - const settings = getSettings(); - if (settings.ui === "shadcn") { - throw new Error("`proofkit add auth` is no longer supported for shadcn projects"); - } - 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/src/cli/add/data-source/deploy-demo-file.ts b/packages/cli/src/cli/add/data-source/deploy-demo-file.ts deleted file mode 100644 index 7d460058..00000000 --- a/packages/cli/src/cli/add/data-source/deploy-demo-file.ts +++ /dev/null @@ -1,96 +0,0 @@ -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.proof.sh/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/src/cli/add/data-source/filemaker.ts b/packages/cli/src/cli/add/data-source/filemaker.ts deleted file mode 100644 index 3e2c4356..00000000 --- a/packages/cli/src/cli/add/data-source/filemaker.ts +++ /dev/null @@ -1,437 +0,0 @@ -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, ensureWebviewerFmMcpConfig } from "~/generators/fmdapi.js"; -import { getFmMcpStatus } from "~/helpers/fmMcp.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 fmMcpStatus = await getFmMcpStatus(); - const connectedFileName = fmMcpStatus.connectedFiles[0]; - const localDataSourceName = opts.name ?? "filemaker"; - - if (!opts.server && fmMcpStatus.healthy && connectedFileName) { - addPackageDependency({ - projectDir, - dependencies: ["@proofkit/fmdapi"], - devMode: false, - }); - - await ensureWebviewerFmMcpConfig({ - projectDir, - connectedFileName, - dataSourceName: localDataSourceName, - baseUrl: fmMcpStatus.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 MCP at ${fmMcpStatus.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 MCP connection was detected and no FileMaker server was provided. Start the local FM MCP proxy with a connected file or rerun with --server.", - ); - } - - if (!opts.server) { - const fallbackAction = abortIfCancel( - await p.select({ - message: - "Local FM MCP 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 MCP 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)")}`, - 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?", - 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)`)}`, - 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/src/cli/add/data-source/index.ts b/packages/cli/src/cli/add/data-source/index.ts deleted file mode 100644 index 647db728..00000000 --- a/packages/cli/src/cli/add/data-source/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Command } from "commander"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; - -import { ensureProofKitProject } from "~/cli/utils.js"; -import { 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(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/src/cli/add/fmschema.ts b/packages/cli/src/cli/add/fmschema.ts deleted file mode 100644 index 35430779..00000000 --- a/packages/cli/src/cli/add/fmschema.ts +++ /dev/null @@ -1,214 +0,0 @@ -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", - 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, - 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/src/cli/add/index.ts b/packages/cli/src/cli/add/index.ts deleted file mode 100644 index 19f429e6..00000000 --- a/packages/cli/src/cli/add/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { logger } from "~/utils/logger.js"; -import { runAddAddonAction } from "./addon.js"; - -const ADDON_ONLY_MESSAGE = "Only `proofkit add addon ` is supported."; - -export const runAdd = async (name: string | undefined, options?: { noInstall?: boolean; target?: string }) => { - if (name === "addon") { - return await runAddAddonAction(options?.target); - } - logger.error(ADDON_ONLY_MESSAGE); - throw new Error(ADDON_ONLY_MESSAGE); -}; diff --git a/packages/cli/src/cli/add/page/index.ts b/packages/cli/src/cli/add/page/index.ts deleted file mode 100644 index 6b664273..00000000 --- a/packages/cli/src/cli/add/page/index.ts +++ /dev/null @@ -1,231 +0,0 @@ -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 { 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", - 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(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/src/cli/add/page/post-install/table-infinite.ts b/packages/cli/src/cli/add/page/post-install/table-infinite.ts deleted file mode 100644 index fcccbb27..00000000 --- a/packages/cli/src/cli/add/page/post-install/table-infinite.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/src/cli/add/page/post-install/table.ts b/packages/cli/src/cli/add/page/post-install/table.ts deleted file mode 100644 index a6e930ed..00000000 --- a/packages/cli/src/cli/add/page/post-install/table.ts +++ /dev/null @@ -1,123 +0,0 @@ -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/src/cli/add/page/templates.ts b/packages/cli/src/cli/add/page/templates.ts deleted file mode 100644 index a49d5740..00000000 --- a/packages/cli/src/cli/add/page/templates.ts +++ /dev/null @@ -1,85 +0,0 @@ -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/src/cli/add/page/types.ts b/packages/cli/src/cli/add/page/types.ts deleted file mode 100644 index 7b7da162..00000000 --- a/packages/cli/src/cli/add/page/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/src/cli/deploy/index.ts b/packages/cli/src/cli/deploy/index.ts deleted file mode 100644 index 6ec2b94d..00000000 --- a/packages/cli/src/cli/deploy/index.ts +++ /dev/null @@ -1,489 +0,0 @@ -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 { debugOption, nonInteractiveOption } 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.slice(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(nonInteractiveOption) - .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/src/cli/init.ts b/packages/cli/src/cli/init.ts deleted file mode 100644 index e7e0d46c..00000000 --- a/packages/cli/src/cli/init.ts +++ /dev/null @@ -1,479 +0,0 @@ -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, NODE_RUNTIME_VERSION } from "~/consts.js"; -import { createPnpmWorkspaceFileContent } from "~/core/planInit.js"; -import { addAuth } from "~/generators/auth.js"; -import { runCodegenCommand } from "~/generators/fmdapi.js"; -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { createBareProject } from "~/helpers/createProject.js"; -import { initializeGit } from "~/helpers/git.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import { getIntentInstallCommand } from "~/helpers/intent.js"; -import { logNextSteps } from "~/helpers/logNextSteps.js"; -import { setImportAlias } from "~/helpers/setImportAlias.js"; -import { - getBrowserOxlintConfig, - getHuskyPreCommitHook, - getUltraciteInitCommand, - getWebViewerOxlintConfig, -} from "~/helpers/ultracite.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 { logger } from "~/utils/logger.js"; -import { parseNameAndPath } from "~/utils/parseNameAndPath.js"; -import { type Settings, setSettings } from "~/utils/parseSettings.js"; -import { formatPackageManagerCommand, parseCommandString } from "~/utils/projectFiles.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 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, -}; - -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) - .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(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; - }; - devEngines?: { - packageManager: { - name: string; - version: string; - onFail: "download"; - }; - runtime: { - name: "node"; - version: string; - onFail: "download"; - }; - }; - engines?: { - node: 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); -} - -function createErrorWithCause(message: string, cause: Error): Error { - const wrapped = new Error(message) as Error & { cause?: Error }; - wrapped.cause = cause; - return wrapped; -} - -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 createErrorWithCause( - [ - "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 `npx @proofkit/typegen` later after FileMaker setup is complete.", - ].join("\n"), - rootError, - ); - } - - return createErrorWithCause( - [ - "Post-init generation failed after scaffolding.", - `Project created at: ${projectDir}`, - "Retry `npx @proofkit/typegen` from inside the project once FileMaker settings and connectivity are valid.", - `Underlying error: ${getErrorMessage(error)}`, - ].join("\n"), - 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; - state.ui = "shadcn"; - - 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) - let pkgManagerVersion: string | undefined; - if (pkgManager !== "bun") { - const { stdout } = await execa(pkgManager, ["-v"], { - cwd: projectDir, - }); - pkgManagerVersion = stdout.trim(); - pkgJson.packageManager = undefined; - pkgJson.devEngines = { - packageManager: { - name: pkgManager, - version: pkgManagerVersion, - onFail: "download", - }, - runtime: { - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }, - }; - } - pkgJson.engines = { - node: NODE_RUNTIME_VERSION, - }; - - fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, { - spaces: 2, - }); - if (pkgManager === "pnpm") { - fs.writeFileSync( - path.join(projectDir, "pnpm-workspace.yaml"), - createPnpmWorkspaceFileContent(state.appType ?? "browser"), - "utf8", - ); - } - - // Ensure proofkit.json exists with shadcn settings - const initialSettings: Settings = { - 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 }); - } - - const ultraciteCommand = getUltraciteInitCommand({ - appType: state.appType ?? "browser", - packageManager: pkgManager, - skipInstall: noInstall, - }); - await execa(ultraciteCommand.command, ultraciteCommand.args, { - cwd: projectDir, - stdio: "pipe", - }); - const oxlintConfigContent = - (state.appType ?? "browser") === "browser" ? getBrowserOxlintConfig() : getWebViewerOxlintConfig(); - fs.writeFileSync(path.join(projectDir, "oxlint.config.ts"), oxlintConfigContent, "utf8"); - fs.writeFileSync(path.join(projectDir, ".husky/pre-commit"), getHuskyPreCommitHook(), "utf8"); - const intentCommand = getIntentInstallCommand(pkgManager); - await execa(intentCommand.command, intentCommand.args, { - cwd: projectDir, - stdio: "pipe", - }); - - 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 (!noInstall) { - const fixCommandString = formatPackageManagerCommand(pkgManager, "fix"); - const [fixCommand, ...fixArgs] = parseCommandString(fixCommandString); - if (!fixCommand) { - throw new Error(`Unable to resolve fix command for ${pkgManager}.`); - } - await execa(fixCommand, fixArgs, { - cwd: projectDir, - stdio: "pipe", - }).catch((error: unknown) => { - if (state.debug) { - logger.warn(`Fix command failed; continuing. packageManager=${pkgManager} command=${fixCommandString}`); - logger.error(error); - } - }); - - const lintCommandString = formatPackageManagerCommand(pkgManager, "lint"); - const [lintCommand, ...lintArgs] = parseCommandString(lintCommandString); - if (!lintCommand) { - throw new Error(`Unable to resolve lint command for ${pkgManager}.`); - } - await execa(lintCommand, lintArgs, { - cwd: projectDir, - stdio: "pipe", - }).catch((error: unknown) => { - logger.warn(`Lint did not succeed; continuing setup. packageManager=${pkgManager} command=${lintCommandString}`); - if (state.debug) { - logger.error(error); - } - }); - } - - if (!noGit) { - await initializeGit(projectDir); - } - - logNextSteps({ - projectName: appDir, - noInstall, - }); -}; diff --git a/packages/cli/src/cli/menu.ts b/packages/cli/src/cli/menu.ts deleted file mode 100644 index 8a0ce42e..00000000 --- a/packages/cli/src/cli/menu.ts +++ /dev/null @@ -1,96 +0,0 @@ -import chalk from "chalk"; -import { Effect } from "effect"; -import open from "open"; -import { confirm, log, select } from "~/cli/prompts.js"; - -import { DOCS_URL } from "~/consts.js"; -import { runDoctor } from "~/core/doctor.js"; -import { runPrompt } from "~/core/prompt.js"; -import { makeLiveLayer } from "~/services/live.js"; -import { checkForAvailableUpgrades, runAllAvailableUpgrades } from "~/upgrades/index.js"; -import { resolveNonInteractiveMode } from "~/utils/nonInteractive.js"; -import { runDeploy } from "./deploy/index.js"; -import { abortIfCancel } from "./utils.js"; - -function getMenuRuntime() { - return makeLiveLayer({ - cwd: process.cwd(), - debug: false, - nonInteractive: resolveNonInteractiveMode({ - stdinIsTTY: process.stdin?.isTTY, - stdoutIsTTY: process.stdout?.isTTY, - }), - }); -} - -export const runMenu = async () => { - 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: "Doctor", - value: "doctor", - hint: "Inspect project health and get exact next steps", - }, - { - label: "Prompt", - value: "prompt", - hint: "Reserved AI-agent workflow entrypoint", - }, - { - label: "Deploy", - value: "deploy", - hint: "Deploy your app to Vercel", - }, - { - label: "View Documentation", - value: "docs", - hint: "Open ProofKit documentation", - }, - ], - }), - ); - - switch (menuChoice) { - case "doctor": - await Effect.runPromise(getMenuRuntime()(runDoctor)); - break; - case "prompt": - await Effect.runPromise(getMenuRuntime()(runPrompt)); - break; - case "docs": - log.info(`Opening ${chalk.cyan(DOCS_URL)} in your browser...`); - await open(DOCS_URL); - break; - case "deploy": - await runDeploy(); - break; - default: - throw new Error(`Unknown menu choice: ${menuChoice}`); - } -}; diff --git a/packages/cli/src/cli/react-email.ts b/packages/cli/src/cli/react-email.ts deleted file mode 100644 index f0ac245a..00000000 --- a/packages/cli/src/cli/react-email.ts +++ /dev/null @@ -1,27 +0,0 @@ -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/src/cli/remove/data-source.ts b/packages/cli/src/cli/remove/data-source.ts deleted file mode 100644 index 3118c1bd..00000000 --- a/packages/cli/src/cli/remove/data-source.ts +++ /dev/null @@ -1,152 +0,0 @@ -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 { UserCancelledError } from "~/core/errors.js"; -import { removeFromFmschemaConfig, runCodegenCommand } from "~/generators/fmdapi.js"; -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { type DataSource, getSettings, setSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel, ensureProofKitProject } 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 UserCancelledError({ message: "User aborted the operation" }); - } - } - - // 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(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/src/cli/remove/index.ts b/packages/cli/src/cli/remove/index.ts deleted file mode 100644 index a825e6c5..00000000 --- a/packages/cli/src/cli/remove/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Command } from "commander"; -import * as p from "~/cli/prompts.js"; - -import { 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(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/src/cli/remove/page.ts b/packages/cli/src/cli/remove/page.ts deleted file mode 100644 index 23137ee3..00000000 --- a/packages/cli/src/cli/remove/page.ts +++ /dev/null @@ -1,218 +0,0 @@ -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 { debugOption, nonInteractiveOption } 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) { - if (state.nonInteractive) { - throw new Error("Route is required in non-interactive mode."); - } - - 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(nonInteractiveOption) - .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/src/cli/remove/schema.ts b/packages/cli/src/cli/remove/schema.ts deleted file mode 100644 index 4cc40088..00000000 --- a/packages/cli/src/cli/remove/schema.ts +++ /dev/null @@ -1,100 +0,0 @@ -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/src/cli/tanstack-query.ts b/packages/cli/src/cli/tanstack-query.ts deleted file mode 100644 index fb29fac0..00000000 --- a/packages/cli/src/cli/tanstack-query.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/src/cli/typegen/index.ts b/packages/cli/src/cli/typegen/index.ts index 23a4f61c..b1fab6b3 100644 --- a/packages/cli/src/cli/typegen/index.ts +++ b/packages/cli/src/cli/typegen/index.ts @@ -1,20 +1,30 @@ -import { Command } from "commander"; +import { runCli } from "@proofkit/typegen/cli"; -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 interface TypegenOptions { + config?: string; + envPath?: string; + proofkitToken?: string; + resetOverrides?: boolean; } -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; -}; +/** + * Thin alias to the `@proofkit/typegen` CLI. All config reading, validation, and + * generation lives in that package; we only map flags to its arg list so there is + * no duplicated logic to drift. + */ +export async function runTypegen(options: TypegenOptions = {}) { + const args: string[] = []; + if (options.config) { + args.push("--config", options.config); + } + if (options.envPath) { + args.push("--env-path", options.envPath); + } + if (options.proofkitToken) { + args.push("--proofkit-token", options.proofkitToken); + } + if (options.resetOverrides) { + args.push("--reset-overrides"); + } + await runCli(args); +} diff --git a/packages/cli/src/cli/update/index.ts b/packages/cli/src/cli/update/index.ts deleted file mode 100644 index 93eca92b..00000000 --- a/packages/cli/src/cli/update/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -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/src/cli/update/makeUpgradeCommand.ts b/packages/cli/src/cli/update/makeUpgradeCommand.ts deleted file mode 100644 index c3378db0..00000000 --- a/packages/cli/src/cli/update/makeUpgradeCommand.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Command } from "commander"; - -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") - .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/src/core/prompt.ts b/packages/cli/src/core/prompt.ts deleted file mode 100644 index 386ae8f4..00000000 --- a/packages/cli/src/core/prompt.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Effect } from "effect"; -import { DOCS_URL } from "~/consts.js"; -import { ConsoleService } from "~/core/context.js"; - -export const runPrompt = Effect.gen(function* () { - const consoleService = yield* ConsoleService; - - consoleService.note( - [ - "Agent-ready prompts are coming soon.", - "", - "This command will become the stable entrypoint for docs-linked AI workflows.", - `For now, use package-native tools directly and check docs: ${DOCS_URL}/docs/cli`, - ].join("\n"), - "Coming soon", - ); -}); diff --git a/packages/cli/src/generators/auth.ts b/packages/cli/src/generators/auth.ts deleted file mode 100644 index 7c366c9f..00000000 --- a/packages/cli/src/generators/auth.ts +++ /dev/null @@ -1,83 +0,0 @@ -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("`proofkit add auth` is no longer supported for shadcn projects"); - } - 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/src/generators/fmdapi.ts b/packages/cli/src/generators/fmdapi.ts deleted file mode 100644 index 27602c99..00000000 --- a/packages/cli/src/generators/fmdapi.ts +++ /dev/null @@ -1,525 +0,0 @@ -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/lib/esm/main.js"; -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.proof.sh/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.proof.sh/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.proof.sh/typegen-config-schema.json", - config: configsToAdd, - }; - } - - await writeJsonConfigFile(jsonConfigPath, fileContent); - - if (runCodegen) { - await runCodegenCommand(); - } -} - -export async function ensureWebviewerFmMcpConfig({ - 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: [], - fmMcp: { - enabled: true, - ...(baseUrl ? { baseUrl } : {}), - ...(connectedFileName ? { connectedFileName } : {}), - }, - }; - - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - fileContent = { - $schema: "https://proofkit.proof.sh/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 ?? [], - fmMcp: { - enabled: true, - ...(existingConfig.fmMcp ?? {}), - ...(newConfig.fmMcp ?? {}), - }, - }; - } - - 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.proof.sh/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/src/generators/route.ts b/packages/cli/src/generators/route.ts deleted file mode 100644 index e008a05c..00000000 --- a/packages/cli/src/generators/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -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., Web Viewer 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/src/generators/tanstack-query.ts b/packages/cli/src/generators/tanstack-query.ts deleted file mode 100644 index 874eee0d..00000000 --- a/packages/cli/src/generators/tanstack-query.ts +++ /dev/null @@ -1,97 +0,0 @@ -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/src/globalOptions.ts b/packages/cli/src/globalOptions.ts deleted file mode 100644 index 9435e511..00000000 --- a/packages/cli/src/globalOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Option } from "commander"; - -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/src/helpers/createProject.ts b/packages/cli/src/helpers/createProject.ts deleted file mode 100644 index 71a8ca24..00000000 --- a/packages/cli/src/helpers/createProject.ts +++ /dev/null @@ -1,112 +0,0 @@ -import path from "node:path"; - -import { getAgentInstructions } from "~/consts.js"; -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: ["@types/node"], - devMode: true, - }); - - // Add base deps for current templates. Legacy Mantine projects remain supported elsewhere. - 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 = ["oxlint", "ultracite"] as AvailableDependencies[]; - const VITE_SHADCN_BASE_DEV_DEPS = ["@proofkit/typegen", "oxlint", "ultracite"] as AvailableDependencies[]; - - 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 scaffold 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); - replaceTextInFiles(state.projectDir, "__PACKAGE_MANAGER__", pkgManager); - replaceTextInFiles(state.projectDir, "__AGENT_INSTRUCTIONS__", getAgentInstructions()); - - return state.projectDir; -}; diff --git a/packages/cli/src/helpers/fmMcp.ts b/packages/cli/src/helpers/fmMcp.ts deleted file mode 100644 index ab58114e..00000000 --- a/packages/cli/src/helpers/fmMcp.ts +++ /dev/null @@ -1,56 +0,0 @@ -const defaultBaseUrl = process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365"; -const REQUEST_TIMEOUT_MS = 3000; - -export interface FmMcpStatus { - 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 getFmMcpStatus(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 getFmMcpStatus(baseUrl); - return status.connectedFiles[0]; -} diff --git a/packages/cli/src/helpers/git.ts b/packages/cli/src/helpers/git.ts deleted file mode 100644 index bdeaefee..00000000 --- a/packages/cli/src/helpers/git.ts +++ /dev/null @@ -1,140 +0,0 @@ -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/src/helpers/installDependencies.ts b/packages/cli/src/helpers/installDependencies.ts deleted file mode 100644 index 880bd436..00000000 --- a/packages/cli/src/helpers/installDependencies.ts +++ /dev/null @@ -1,242 +0,0 @@ -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/src/helpers/installPackages.ts b/packages/cli/src/helpers/installPackages.ts deleted file mode 100644 index 06345c47..00000000 --- a/packages/cli/src/helpers/installPackages.ts +++ /dev/null @@ -1,25 +0,0 @@ -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/src/helpers/logNextSteps.ts b/packages/cli/src/helpers/logNextSteps.ts deleted file mode 100644 index c8ae3b11..00000000 --- a/packages/cli/src/helpers/logNextSteps.ts +++ /dev/null @@ -1,44 +0,0 @@ -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")}`); - } -}; diff --git a/packages/cli/src/helpers/replaceText.ts b/packages/cli/src/helpers/replaceText.ts deleted file mode 100644 index e7f9d4b1..00000000 --- a/packages/cli/src/helpers/replaceText.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/src/helpers/scaffoldProject.ts b/packages/cli/src/helpers/scaffoldProject.ts deleted file mode 100644 index f5667760..00000000 --- a/packages/cli/src/helpers/scaffoldProject.ts +++ /dev/null @@ -1,132 +0,0 @@ -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/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")); - fs.writeFileSync(path.join(projectDir, ".cursorignore"), "CLAUDE.md\n", "utf8"); - - const scaffoldedName = projectName === "." ? "App" : chalk.cyan.bold(projectName); - - spinner.succeed(`${scaffoldedName} ${chalk.green("scaffolded successfully!")}\n`); -}; diff --git a/packages/cli/src/helpers/selectBoilerplate.ts b/packages/cli/src/helpers/selectBoilerplate.ts deleted file mode 100644 index 4b538d3d..00000000 --- a/packages/cli/src/helpers/selectBoilerplate.ts +++ /dev/null @@ -1,32 +0,0 @@ -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/src/helpers/setImportAlias.ts b/packages/cli/src/helpers/setImportAlias.ts deleted file mode 100644 index 7551134b..00000000 --- a/packages/cli/src/helpers/setImportAlias.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/src/helpers/stealth-init.ts b/packages/cli/src/helpers/stealth-init.ts deleted file mode 100644 index 6f865ab8..00000000 --- a/packages/cli/src/helpers/stealth-init.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/src/helpers/version-fetcher.ts b/packages/cli/src/helpers/version-fetcher.ts deleted file mode 100644 index 26a21e80..00000000 --- a/packages/cli/src/helpers/version-fetcher.ts +++ /dev/null @@ -1,131 +0,0 @@ -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/src/index.ts b/packages/cli/src/index.ts index 3e669747..d40edbba 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -34,7 +34,6 @@ import { runDoctor } from "~/core/doctor.js"; import { getCliErrorMessage, isCliError, NonInteractiveInputError, UserCancelledError } from "~/core/errors.js"; import { executeInitPlan } from "~/core/executeInitPlan.js"; import { planInit } from "~/core/planInit.js"; -import { runPrompt } from "~/core/prompt.js"; import { resolveInitRequest } from "~/core/resolveInitRequest.js"; import type { CliFlags } from "~/core/types.js"; import { CLI_VERSION } from "~/package-versions.js"; @@ -80,7 +79,7 @@ export const runInit = (name?: string, rawFlags?: Partial) => return { request, plan }; }); -type ProjectMenuChoice = "add" | "remove" | "typegen" | "deploy" | "upgrade" | "doctor" | "prompt" | "docs"; +type ProjectMenuChoice = "typegen" | "doctor" | "docs"; function isPromptCancellationError(error: unknown) { return error instanceof UserCancelledError || (error instanceof Error && error.name === "ExitPromptError"); @@ -105,41 +104,16 @@ const runProjectMenu = Effect.gen(function* () { prompt.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: "Doctor", value: "doctor", hint: "Inspect project health and next steps", }, - { - label: "Prompt", - value: "prompt", - hint: "Show agent workflow guidance", - }, { label: "View Documentation", value: "docs", @@ -154,10 +128,13 @@ const runProjectMenu = Effect.gen(function* () { }); switch (menuChoice) { + case "typegen": + return yield* Effect.promise(async () => { + const { runTypegen } = await import("~/cli/typegen/index.js"); + await runTypegen(); + }); case "doctor": return yield* runDoctor; - case "prompt": - return yield* runPrompt; case "docs": { const { DOCS_URL } = yield* Effect.promise(() => import("~/consts.js")); consoleService.info(`Opening ${DOCS_URL} in your browser...`); @@ -165,66 +142,6 @@ const runProjectMenu = Effect.gen(function* () { yield* Effect.promise(() => open(DOCS_URL)); return; } - case "add": - return yield* Effect.tryPromise({ - try: async () => { - const [{ runAdd }, { initProgramState, state }] = await Promise.all([ - import("~/cli/add/index.js"), - import("~/state.js"), - ]); - initProgramState({}); - state.baseCommand = "add"; - state.projectDir = process.cwd(); - await runAdd(undefined); - }, - catch: (cause) => toProjectMenuCommandError("add", cause), - }); - case "remove": - return yield* Effect.tryPromise({ - try: async () => { - const [{ runRemove }, { initProgramState, state }] = await Promise.all([ - import("~/cli/remove/index.js"), - import("~/state.js"), - ]); - initProgramState({}); - state.baseCommand = "remove"; - state.projectDir = process.cwd(); - await runRemove(undefined); - }, - catch: (cause) => toProjectMenuCommandError("remove", cause), - }); - case "typegen": - return yield* Effect.promise(async () => { - const [{ runTypegen }, { getSettings }, { state }] = await Promise.all([ - import("~/cli/typegen/index.js"), - import("~/utils/parseSettings.js"), - import("~/state.js"), - ]); - state.projectDir = process.cwd(); - await runTypegen({ settings: getSettings() }); - }); - case "deploy": - return yield* Effect.promise(async () => { - const [{ runDeploy }, { initProgramState, state }] = await Promise.all([ - import("~/cli/deploy/index.js"), - import("~/state.js"), - ]); - initProgramState({}); - state.baseCommand = "deploy"; - state.projectDir = process.cwd(); - await runDeploy(); - }); - case "upgrade": - return yield* Effect.promise(async () => { - const [{ runUpgrade }, { initProgramState, state }] = await Promise.all([ - import("~/cli/update/index.js"), - import("~/state.js"), - ]); - initProgramState({}); - state.baseCommand = "upgrade"; - state.projectDir = process.cwd(); - await runUpgrade(); - }); default: throw new Error(`Unknown menu choice: ${menuChoice}`); } @@ -247,8 +164,8 @@ export const runDefaultCommand = (rawFlags?: Partial) => consoleService.note( [ - "ProofKit now focuses on project bootstrap, diagnostics, and agent entrypoints.", - "Use an explicit command such as `proofkit doctor`, `proofkit prompt`, or `proofkit init`.", + "ProofKit now focuses on project bootstrap and diagnostics.", + "Use an explicit command such as `proofkit doctor`, `proofkit typegen`, or `proofkit init`.", ].join("\n"), "Project commands", ); @@ -290,19 +207,6 @@ function getCurrentTTYState() { }; } -function legacyEffect(runLegacy: () => Promise, options?: { nonInteractive?: boolean; debug?: boolean }) { - const nonInteractive = resolveNonInteractiveMode({ - nonInteractive: options?.nonInteractive, - ...getCurrentTTYState(), - }); - - return makeLiveLayer({ - cwd: process.cwd(), - debug: options?.debug === true, - nonInteractive, - })(Effect.promise(runLegacy)); -} - function makeInitCommand() { return makeCommand( "init", @@ -370,147 +274,28 @@ function makeInitCommand() { ).pipe(withCommandDescription("Create a new project with ProofKit")); } -function makeAddCommand() { - return makeCommand( - "add", - { - name: optionalArg(textArg({ name: "name" })).pipe(withArgDescription("Supported add target, currently `addon`")), - target: optionalArg(textArg({ name: "target" })).pipe(withArgDescription("Add-on target")), - noInstall: booleanOption("no-install").pipe(withOptionDescription("Skip package installation")), - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ name, target, noInstall, CI, nonInteractive, debug }) => - legacyEffect( - async () => { - const [{ runAdd }, { initProgramState, state }] = await Promise.all([ - import("~/cli/add/index.js"), - import("~/state.js"), - ]); - initProgramState({ - noInstall, - ci: CI, - nonInteractive, - debug, - }); - state.baseCommand = "add"; - state.projectDir = process.cwd(); - await runAdd(getOrUndefined(name), { - noInstall, - target: getOrUndefined(target), - }); - }, - { nonInteractive: CI || nonInteractive, debug }, - ), - ).pipe(withCommandDescription("Add a supported ProofKit add-on.")); -} - -function makeRemoveCommand() { - return makeCommand( - "remove", - { - name: optionalArg(textArg({ name: "name" })).pipe(withArgDescription("Component type to remove")), - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ name, CI, nonInteractive, debug }) => - legacyEffect( - async () => { - const [{ runRemove }, { initProgramState, state }] = await Promise.all([ - import("~/cli/remove/index.js"), - import("~/state.js"), - ]); - initProgramState({ - ci: CI, - nonInteractive, - debug, - }); - state.baseCommand = "remove"; - state.projectDir = process.cwd(); - await runRemove(getOrUndefined(name)); - }, - { nonInteractive: CI || nonInteractive, debug }, - ), - ).pipe(withCommandDescription("Legacy command. Prefer direct code edits or package-native tools.")); -} - function makeTypegenCommand() { return makeCommand( "typegen", { - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ debug }) => - legacyEffect( - async () => { - const [{ runTypegen }, { state }] = await Promise.all([ - import("~/cli/typegen/index.js"), - import("~/state.js"), - ]); - state.projectDir = process.cwd(); - await runTypegen({ - settings: (await import("~/utils/parseSettings.js")).getSettings(), - }); - }, - { debug }, + config: optionalTextOption("config", "Optional typegen config file name"), + envPath: optionalTextOption("env-path", "Optional path to your .env file"), + proofkitToken: optionalTextOption("proofkit-token", "Transient ProofKit token for FM MCP authorization"), + resetOverrides: booleanOption("reset-overrides").pipe( + withOptionDescription("Recreate the overrides file(s) even if they already exist"), ), - ).pipe(withCommandDescription("Legacy alias. Prefer `npx @proofkit/typegen`.")); -} - -function makeDeployCommand() { - return makeCommand( - "deploy", - { - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), }, - ({ debug }) => - legacyEffect( - async () => { - const [{ runDeploy }, { initProgramState, state }] = await Promise.all([ - import("~/cli/deploy/index.js"), - import("~/state.js"), - ]); - initProgramState({ debug }); - state.baseCommand = "deploy"; - state.projectDir = process.cwd(); - await runDeploy(); - }, - { debug }, - ), - ).pipe(withCommandDescription("Deploy your app")); -} - -function makeUpgradeCommand() { - return makeCommand( - "upgrade", - { - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ CI, nonInteractive, debug }) => - legacyEffect( - async () => { - const [{ runUpgrade }, { initProgramState, state }] = await Promise.all([ - import("~/cli/update/index.js"), - import("~/state.js"), - ]); - initProgramState({ ci: CI, nonInteractive, debug }); - state.baseCommand = "upgrade"; - state.projectDir = process.cwd(); - await runUpgrade(); - }, - { nonInteractive: CI || nonInteractive, debug }, - ), - ).pipe(withCommandDescription("Legacy command.")); + ({ config, envPath, proofkitToken, resetOverrides }) => + Effect.promise(async () => { + const { runTypegen } = await import("~/cli/typegen/index.js"); + await runTypegen({ + config: getOrUndefined(config), + envPath: getOrUndefined(envPath), + proofkitToken: getOrUndefined(proofkitToken), + resetOverrides, + }); + }), + ).pipe(withCommandDescription("Generate types via @proofkit/typegen")); } function makeDoctorCommand() { @@ -528,21 +313,6 @@ function makeDoctorCommand() { ).pipe(withCommandDescription("Inspect project health and suggest exact next steps")); } -function makePromptCommand() { - return makeCommand( - "prompt", - { - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ debug }) => - makeLiveLayer({ - cwd: process.cwd(), - debug: debug === true, - nonInteractive: true, - })(runPrompt), - ).pipe(withCommandDescription("Agent workflow entrypoint placeholder")); -} - const rootCommand = makeCommand( cliName, { @@ -571,16 +341,7 @@ const rootCommand = makeCommand( ), ).pipe( withCommandDescription("Interactive CLI to scaffold and manage ProofKit projects"), - withSubcommands([ - makeInitCommand(), - makeDoctorCommand(), - makePromptCommand(), - makeAddCommand(), - makeRemoveCommand(), - makeTypegenCommand(), - makeDeployCommand(), - makeUpgradeCommand(), - ]), + withSubcommands([makeInitCommand(), makeDoctorCommand(), makeTypegenCommand()]), ); export const cli = run(rootCommand, { diff --git a/packages/cli/src/installers/auth-shared.ts b/packages/cli/src/installers/auth-shared.ts deleted file mode 100644 index 20f1401d..00000000 --- a/packages/cli/src/installers/auth-shared.ts +++ /dev/null @@ -1,49 +0,0 @@ -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/src/installers/better-auth.ts b/packages/cli/src/installers/better-auth.ts deleted file mode 100644 index f417ab36..00000000 --- a/packages/cli/src/installers/better-auth.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function betterAuthInstaller() { - // TODO: Implement better-auth installer -} diff --git a/packages/cli/src/installers/clerk.ts b/packages/cli/src/installers/clerk.ts deleted file mode 100644 index 11ddd816..00000000 --- a/packages/cli/src/installers/clerk.ts +++ /dev/null @@ -1,153 +0,0 @@ -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/src/installers/dependencyVersionMap.ts b/packages/cli/src/installers/dependencyVersionMap.ts deleted file mode 100644 index 37badaf9..00000000 --- a/packages/cli/src/installers/dependencyVersionMap.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - getFmdapiVersion, - getNodeMajorVersion, - getProofkitBetterAuthVersion, - getProofkitDependencyVersion, - getProofkitWebviewerVersion, - getTypegenVersion, - getVersion, -} from "~/utils/getProofKitVersion.js"; - -/* - * This maps the necessary packages to a version. - * This improves performance significantly over fetching it from the npm registry. - */ -export const dependencyVersionMap = { - "@proofkit/fmdapi": getProofkitDependencyVersion(getFmdapiVersion()), - "@proofkit/webviewer": getProofkitDependencyVersion(getProofkitWebviewerVersion()), - "@proofkit/cli": getProofkitDependencyVersion(getVersion()), - "@proofkit/typegen": getProofkitDependencyVersion(getTypegenVersion()), - "@proofkit/better-auth": getProofkitDependencyVersion(getProofkitBetterAuthVersion()), - - // 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": "^1.16.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", - - // Linting and formatting - oxlint: "^1.39.0", - ultracite: "^7.0.0", - - // Zod - zod: "^4", -} as const; -export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/packages/cli/src/installers/envVars.ts b/packages/cli/src/installers/envVars.ts deleted file mode 100644 index ebcd8cd9..00000000 --- a/packages/cli/src/installers/envVars.ts +++ /dev/null @@ -1,43 +0,0 @@ -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. Initialize them manually before continuing."); - throw new Error("T3 env file not found"); -} diff --git a/packages/cli/src/installers/index.ts b/packages/cli/src/installers/index.ts deleted file mode 100644 index b4fb6fb6..00000000 --- a/packages/cli/src/installers/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/src/installers/install-fm-addon.ts b/packages/cli/src/installers/install-fm-addon.ts index 9de407c0..621c6e00 100644 --- a/packages/cli/src/installers/install-fm-addon.ts +++ b/packages/cli/src/installers/install-fm-addon.ts @@ -15,7 +15,6 @@ export interface FmAddonInspection { addonName: FmAddonName; addonDir: string; addonDisplayName: string; - installCommand: string; targetDir: string | null; installedPath: string | null; remoteAssetUrl: string; @@ -69,8 +68,8 @@ function getAddonTarget(addonName: FmAddonName): FmAddonTarget { return addonName === "auth" ? "auth" : "webviewer"; } -function getAddonInstallCommand(addonName: FmAddonName) { - return addonName === "auth" ? "proofkit add addon auth" : "proofkit add addon webviewer"; +function getAddonDocsUrl(addonName: FmAddonName) { + return addonName === "auth" ? "https://proofkit.proof.sh/auth/fm-addon" : "https://proofkit.proof.sh/docs/webviewer"; } function getAddonManifestUrl() { @@ -224,7 +223,6 @@ export async function inspectFmAddon( ): Promise { const addonDir = getAddonDir(addonName); const addonDisplayName = getAddonDisplayName(addonName); - const installCommand = getAddonInstallCommand(addonName); const targetDir = options && "targetDir" in options ? options.targetDir : resolveFmAddonDownloadDir(); const remoteAddon = options?.latestAddonPath ? { @@ -248,7 +246,6 @@ export async function inspectFmAddon( addonName, addonDir, addonDisplayName, - installCommand, targetDir: null, installedPath: null, remoteAssetUrl: remoteAddon.remoteAssetUrl, @@ -275,7 +272,6 @@ export async function inspectFmAddon( addonName, addonDir, addonDisplayName, - installCommand, targetDir, installedPath: installedCandidates[0] ?? null, remoteAssetUrl: remoteAddon.remoteAssetUrl, @@ -290,7 +286,6 @@ export async function inspectFmAddon( addonName, addonDir, addonDisplayName, - installCommand, targetDir, installedPath, remoteAssetUrl: remoteAddon.remoteAssetUrl, @@ -307,7 +302,6 @@ export async function inspectFmAddon( addonName, addonDir, addonDisplayName, - installCommand, targetDir, installedPath, remoteAssetUrl: remoteAddon.remoteAssetUrl, @@ -322,7 +316,6 @@ export async function inspectFmAddon( addonName, addonDir, addonDisplayName, - installCommand, targetDir, installedPath, remoteAssetUrl: remoteAddon.remoteAssetUrl, @@ -333,14 +326,12 @@ export async function inspectFmAddon( export function getFmAddonInstallInstructions(addonName: FmAddonName) { const addonDisplayName = getAddonDisplayName(addonName); - const installCommand = getAddonInstallCommand(addonName); + const docsUrl = getAddonDocsUrl(addonName); return { addonDisplayName, - installCommand, - docsUrl: - addonName === "auth" ? "https://proofkit.proof.sh/auth/fm-addon" : "https://proofkit.proof.sh/docs/webviewer", + docsUrl, steps: [ - `Run \`${installCommand}\` to download and open the latest add-on`, + `Download the latest ${addonDisplayName} add-on from ${docsUrl}`, "When FileMaker opens the add-on file, confirm the install prompt", `Open your FileMaker file, go to layout mode, and add the ${addonDisplayName} add-on to the file`, ], diff --git a/packages/cli/src/installers/nextAuth.ts b/packages/cli/src/installers/nextAuth.ts deleted file mode 100644 index 163df0f9..00000000 --- a/packages/cli/src/installers/nextAuth.ts +++ /dev/null @@ -1,189 +0,0 @@ -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.proof.sh/auth/next-auth\n`, - ); -} diff --git a/packages/cli/src/installers/proofkit-auth.ts b/packages/cli/src/installers/proofkit-auth.ts deleted file mode 100644 index 0179e09d..00000000 --- a/packages/cli/src/installers/proofkit-auth.ts +++ /dev/null @@ -1,219 +0,0 @@ -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 } from "~/cli/utils.js"; -import { PKG_ROOT } from "~/consts.js"; -import { UserCancelledError } from "~/core/errors.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, - }), - ); - - if (!shouldContinue) { - throw new UserCancelledError({ message: "User aborted the operation" }); - } - } - } - 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/src/installers/proofkit-webviewer.ts b/packages/cli/src/installers/proofkit-webviewer.ts index 52743e31..b11d8ab9 100644 --- a/packages/cli/src/installers/proofkit-webviewer.ts +++ b/packages/cli/src/installers/proofkit-webviewer.ts @@ -70,6 +70,9 @@ export function getWebViewerAddonMessages({ hasRequiredLayouts, inspection }: We nextSteps: [] as string[], }; + const instructions = getFmAddonInstallInstructions(inspection.addonName); + const { docsUrl } = instructions; + if (hasRequiredLayouts) { messages.info.push("Successfully detected all required layouts for ProofKit Web Viewer in your FileMaker file."); } @@ -79,10 +82,8 @@ export function getWebViewerAddonMessages({ hasRequiredLayouts, inspection }: We inspection.installedVersion && inspection.latestVersion ? ` Local version: ${inspection.installedVersion}. Latest version: ${inspection.latestVersion}.` : ""; - messages.warn.push( - `New ProofKit Web Viewer add-on available. Run \`${inspection.installCommand}\` to download and open it.${versionSuffix}`, - ); - messages.nextSteps.push(inspection.installCommand); + messages.warn.push(`New ProofKit Web Viewer add-on available. See ${docsUrl} to update it.${versionSuffix}`); + messages.nextSteps.push(`Update the ProofKit Web Viewer add-on: ${docsUrl}`); } if (inspection.status === "unknown" && inspection.reason === "unsupported-platform") { @@ -90,25 +91,24 @@ export function getWebViewerAddonMessages({ hasRequiredLayouts, inspection }: We } if (hasRequiredLayouts === false) { - const instructions = getFmAddonInstallInstructions("wv"); messages.warn.push( "ProofKit Web Viewer layouts were not detected in your FileMaker file. The add-on may not be installed in the file yet.", ); if (inspection.status === "missing") { messages.warn.push( - `Local ProofKit Web Viewer add-on file was not found. Run \`${inspection.installCommand}\` to download and open it.`, + `Local ProofKit Web Viewer add-on file was not found. See ${docsUrl} to download and install it.`, ); - messages.nextSteps.push(inspection.installCommand); + messages.nextSteps.push(`Install the ProofKit Web Viewer add-on: ${docsUrl}`); } if (inspection.status === "unknown" && inspection.reason !== "unsupported-platform") { messages.warn.push( "Could not determine the local ProofKit Web Viewer add-on version. Reinstall it explicitly if you need the latest local files.", ); - messages.nextSteps.push(inspection.installCommand); + messages.nextSteps.push(`Install the ProofKit Web Viewer add-on: ${docsUrl}`); } messages.info.push( chalk.bgYellow(" ACTION REQUIRED: ") + - ` Install or update the ProofKit Web Viewer add-on in your FileMaker file. ${chalk.dim(`(Learn more: ${instructions.docsUrl})`)}`, + ` Install or update the ProofKit Web Viewer add-on in your FileMaker file. ${chalk.dim(`(Learn more: ${docsUrl})`)}`, ); for (const step of instructions.steps) { messages.info.push(step); diff --git a/packages/cli/src/installers/react-email.ts b/packages/cli/src/installers/react-email.ts deleted file mode 100644 index 9e59d2f6..00000000 --- a/packages/cli/src/installers/react-email.ts +++ /dev/null @@ -1,209 +0,0 @@ -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", - )}`, - }), - ); - } - - 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`, - )}`, - }), - ); - } - - 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/src/upgrades/cursorRules.ts b/packages/cli/src/upgrades/cursorRules.ts deleted file mode 100644 index 1338225f..00000000 --- a/packages/cli/src/upgrades/cursorRules.ts +++ /dev/null @@ -1,41 +0,0 @@ -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/src/upgrades/index.ts b/packages/cli/src/upgrades/index.ts deleted file mode 100644 index b72dbc98..00000000 --- a/packages/cli/src/upgrades/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -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/src/upgrades/shadcn.ts b/packages/cli/src/upgrades/shadcn.ts deleted file mode 100644 index ea10bfd0..00000000 --- a/packages/cli/src/upgrades/shadcn.ts +++ /dev/null @@ -1,55 +0,0 @@ -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-shadcn"); - - // 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")); - await fs.ensureDir(path.join(projectDir, "src/utils")); - fs.copySync(path.join(TEMPLATE_ROOT, "src/utils/styles.ts"), path.join(projectDir, "src/utils/styles.ts")); - await fs.ensureDir(path.join(projectDir, "src/config/theme")); - 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/src/utils/addPackageDependency.ts b/packages/cli/src/utils/addPackageDependency.ts deleted file mode 100644 index 304d9f2a..00000000 --- a/packages/cli/src/utils/addPackageDependency.ts +++ /dev/null @@ -1,32 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import type { PackageJson } from "type-fest"; - -import { type AvailableDependencies, dependencyVersionMap } from "~/installers/dependencyVersionMap.js"; -import { state } from "~/state.js"; -import { sortPackageJson } from "~/utils/sortPackageJson.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/src/utils/addToEnvs.ts b/packages/cli/src/utils/addToEnvs.ts deleted file mode 100644 index 5af7e131..00000000 --- a/packages/cli/src/utils/addToEnvs.ts +++ /dev/null @@ -1,131 +0,0 @@ -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/src/utils/formatting.ts b/packages/cli/src/utils/formatting.ts deleted file mode 100644 index 2c491be8..00000000 --- a/packages/cli/src/utils/formatting.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 ultracite 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 ultracite fix on the project directory - await execa("npx", ["ultracite", "fix", "."], { - cwd: state.projectDir, - }); - } catch (error) { - if (state.debug) { - console.log("Error formatting files with ultracite"); - console.error(error); - } - // Continue even if formatting fails - } -} diff --git a/packages/cli/src/utils/getUserPkgManager.ts b/packages/cli/src/utils/getUserPkgManager.ts deleted file mode 100644 index d2e3afdc..00000000 --- a/packages/cli/src/utils/getUserPkgManager.ts +++ /dev/null @@ -1,21 +0,0 @@ -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/src/utils/isTTYError.ts b/packages/cli/src/utils/isTTYError.ts deleted file mode 100644 index ccf602ed..00000000 --- a/packages/cli/src/utils/isTTYError.ts +++ /dev/null @@ -1 +0,0 @@ -export class IsTTYError extends Error {} diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts deleted file mode 100644 index 3ddb9775..00000000 --- a/packages/cli/src/utils/logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/src/utils/proofkitReleaseChannel.ts b/packages/cli/src/utils/proofkitReleaseChannel.ts deleted file mode 100644 index aa7ecf17..00000000 --- a/packages/cli/src/utils/proofkitReleaseChannel.ts +++ /dev/null @@ -1,93 +0,0 @@ -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/src/utils/renderVersionWarning.ts b/packages/cli/src/utils/renderVersionWarning.ts deleted file mode 100644 index fd046831..00000000 --- a/packages/cli/src/utils/renderVersionWarning.ts +++ /dev/null @@ -1,86 +0,0 @@ -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/src/utils/ts-morph.ts b/packages/cli/src/utils/ts-morph.ts deleted file mode 100644 index 92d58954..00000000 --- a/packages/cli/src/utils/ts-morph.ts +++ /dev/null @@ -1,25 +0,0 @@ -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/src/utils/validateImportAlias.ts b/packages/cli/src/utils/validateImportAlias.ts deleted file mode 100644 index bd33ca61..00000000 --- a/packages/cli/src/utils/validateImportAlias.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const validateImportAlias = (input: string) => { - if (input.startsWith(".") || input.startsWith("/")) { - return "Import alias can't start with '.' or '/'"; - } - return; -}; diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index 52a4cb81..fb94995b 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -1,7 +1,7 @@ import { execFileSync, spawnSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath } from "node:url"; import fs from "fs-extra"; import { describe, expect, it } from "vitest"; @@ -59,7 +59,7 @@ describe("proofkit CLI", () => { expect(output).toContain("Found"); expect(output).toContain("Project commands"); expect(output).toContain("proofkit doctor"); - expect(output).toContain("proofkit prompt"); + expect(output).toContain("proofkit typegen"); }); it("fails with guidance when no command is used in non-interactive mode", () => { @@ -147,9 +147,7 @@ describe("proofkit CLI", () => { const output = `${result.stdout}\n${result.stderr}`; expect(result.status).not.toBe(0); - expect(output).toContain( - "Invalid subcommand for proofkit - use one of 'init', 'doctor', 'prompt', 'add', 'remove', 'typegen', 'deploy', 'upgrade'", - ); + expect(output).toContain("Invalid subcommand for proofkit - use one of 'init', 'doctor', 'typegen'"); expect(output).not.toContain('"CommandMismatch"'); expect(output).not.toContain("[debug]"); }); @@ -164,72 +162,8 @@ describe("proofkit CLI", () => { const output = `${result.stdout}\n${result.stderr}`; expect(result.status).not.toBe(0); - expect(output).toContain( - "Invalid subcommand for proofkit - use one of 'init', 'doctor', 'prompt', 'add', 'remove', 'typegen', 'deploy', 'upgrade'", - ); + expect(output).toContain("Invalid subcommand for proofkit - use one of 'init', 'doctor', 'typegen'"); expect(output).toContain("[debug]"); expect(output).toContain('"CommandMismatch"'); }); - - it("supports `proofkit prompt`", () => { - const result = spawnSync("node", [distEntry, "prompt"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("Agent-ready prompts are coming soon."); - }); - - it("supports `proofkit add addon webviewer`", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-addon-project-")); - const addonDownloadDir = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-addon-downloads-")); - const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-addon-fixtures-")); - const addonFixturePath = path.join(fixtureDir, "ProofKit.fmaddon"); - await fs.writeFile(addonFixturePath, "fake-fmaddon"); - const manifestPath = path.join(fixtureDir, "manifest.json"); - await fs.writeJson(manifestPath, { - latestVersion: "2.2.4.0", - versions: [ - { - version: "2.2.4.0", - assets: [ - { - file: "ProofKit.fmaddon", - url: pathToFileURL(addonFixturePath).toString(), - }, - ], - }, - ], - }); - - const result = spawnSync("node", [distEntry, "add", "addon", "webviewer", "--non-interactive"], { - cwd, - stdio: "pipe", - encoding: "utf8", - env: { - ...process.env, - PROOFKIT_FM_ADDON_MANIFEST_URL: pathToFileURL(manifestPath).toString(), - PROOFKIT_FM_ADDON_DOWNLOAD_DIR: addonDownloadDir, - PROOFKIT_SKIP_OPEN_FM_ADDON: "1", - }, - }); - - expect(result.status).toBe(0); - - expect(await fs.pathExists(path.join(addonDownloadDir, "ProofKit.fmaddon"))).toBe(true); - expect(await fs.pathExists(path.join(addonDownloadDir, "ProofKit.fmaddon.proofkit.json"))).toBe(true); - }); - - it("rejects unsupported add targets", () => { - const result = spawnSync("node", [distEntry, "add", "page"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).not.toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("Only `proofkit add addon ` is supported."); - }); }); diff --git a/packages/cli/tests/default-command.test.ts b/packages/cli/tests/default-command.test.ts index 98d578ec..d6ca6aa0 100644 --- a/packages/cli/tests/default-command.test.ts +++ b/packages/cli/tests/default-command.test.ts @@ -80,7 +80,7 @@ describe("default command routing", () => { expect(promptTranscript.select).toEqual([ { message: "What would you like to do?", - options: ["add", "remove", "typegen", "deploy", "upgrade", "doctor", "prompt", "docs"], + options: ["typegen", "doctor", "docs"], }, ]); expect(consoleTranscript.note.some((entry) => entry.title === "Project commands")).toBe(false); diff --git a/packages/cli/tests/doctor.test.ts b/packages/cli/tests/doctor.test.ts index d9c7bbb4..0aa2fe71 100644 --- a/packages/cli/tests/doctor.test.ts +++ b/packages/cli/tests/doctor.test.ts @@ -4,7 +4,6 @@ import { Effect } from "effect"; import fs from "fs-extra"; import { describe, expect, it } from "vitest"; import { runDoctor } from "~/core/doctor.js"; -import { runPrompt } from "~/core/prompt.js"; import { makeTestLayer } from "./test-layer.js"; function createConsoleTranscript() { @@ -17,7 +16,7 @@ function createConsoleTranscript() { }; } -describe("doctor and prompt commands", () => { +describe("doctor command", () => { it("reports missing proofkit project", async () => { const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-doctor-missing-")); const consoleTranscript = createConsoleTranscript(); @@ -73,22 +72,4 @@ describe("doctor and prompt commands", () => { expect(consoleTranscript.note[0]?.message).toContain("npx @proofkit/typegen init"); expect(consoleTranscript.note[0]?.message).toContain("npx @proofkit/typegen ui"); }); - - it("returns coming-soon messaging for prompt", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-prompt-")); - const consoleTranscript = createConsoleTranscript(); - - await Effect.runPromise( - runPrompt.pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - console: consoleTranscript, - }), - ), - ); - - expect(consoleTranscript.note[0]?.title).toBe("Coming soon"); - expect(consoleTranscript.note[0]?.message).toContain("Agent-ready prompts are coming soon."); - }); }); diff --git a/packages/cli/tests/init-post-init-generation-errors.test.ts b/packages/cli/tests/init-post-init-generation-errors.test.ts deleted file mode 100644 index 0a40fd41..00000000 --- a/packages/cli/tests/init-post-init-generation-errors.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { createPostInitGenerationError, isMissingTypegenCommandError } from "~/cli/init.js"; - -vi.mock("@inquirer/prompts", () => ({ - checkbox: vi.fn(), - confirm: vi.fn(), - input: vi.fn(), - password: vi.fn(), - search: vi.fn(), - select: vi.fn(), -})); - -describe("init post-init generation error handling", () => { - it("detects missing typegen command failures", () => { - const commandError = new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ); - - expect(isMissingTypegenCommandError(commandError)).toBe(true); - }); - - it("does not classify broad pnpm typegen execution failures as missing command", () => { - const commandError = new Error( - "Command failed with exit code 1: pnpm typegen\nError: connect ECONNREFUSED 127.0.0.1:3000", - ); - - expect(isMissingTypegenCommandError(commandError)).toBe(false); - }); - - it("creates browser-specific guidance for missing typegen command failures", () => { - const commandError = new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ); - - const userFacingError = createPostInitGenerationError({ - error: commandError, - appType: "browser", - projectDir: "/tmp/demo-browser", - }); - - expect(userFacingError.message).toContain("Post-init generation failed after scaffolding."); - expect(userFacingError.message).toContain("Project created at: /tmp/demo-browser"); - expect(userFacingError.message).toContain("browser scaffolds do not define that script"); - expect(userFacingError.message).toContain("npx @proofkit/typegen"); - }); - - it("creates generic recovery guidance for other generation failures", () => { - const commandError = new Error("Unable to read layout metadata"); - - const userFacingError = createPostInitGenerationError({ - error: commandError, - appType: "webviewer", - projectDir: "/tmp/demo-webviewer", - }); - - expect(userFacingError.message).toContain("Post-init generation failed after scaffolding."); - expect(userFacingError.message).toContain("Project created at: /tmp/demo-webviewer"); - expect(userFacingError.message).toContain("Retry `npx @proofkit/typegen`"); - expect(userFacingError.message).toContain("Underlying error: Unable to read layout metadata"); - }); -}); diff --git a/packages/cli/tests/init-run-init-regression.test.ts b/packages/cli/tests/init-run-init-regression.test.ts deleted file mode 100644 index 09502384..00000000 --- a/packages/cli/tests/init-run-init-regression.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const { - createBareProjectMock, - setImportAliasMock, - promptForFileMakerDataSourceMock, - runCodegenCommandMock, - initializeGitMock, - logNextStepsMock, - readJSONSyncMock, - writeJSONSyncMock, - writeFileSyncMock, - execaMock, - mockState, -} = vi.hoisted(() => ({ - createBareProjectMock: vi.fn(), - setImportAliasMock: vi.fn(), - promptForFileMakerDataSourceMock: vi.fn(), - runCodegenCommandMock: vi.fn(), - initializeGitMock: vi.fn(), - logNextStepsMock: vi.fn(), - readJSONSyncMock: vi.fn(), - writeJSONSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - execaMock: vi.fn(), - mockState: { - appType: undefined as "browser" | "webviewer" | undefined, - ui: "shadcn" as const, - projectDir: "/tmp/proofkit-regression", - }, -})); - -vi.mock("@clack/prompts", () => ({ - intro: vi.fn(), - outro: vi.fn(), - note: vi.fn(), - cancel: vi.fn(), - log: { - error: vi.fn(), - info: vi.fn(), - message: vi.fn(), - step: vi.fn(), - success: vi.fn(), - warn: vi.fn(), - }, - spinner: vi.fn(() => ({ - message: vi.fn(), - start: vi.fn(), - stop: vi.fn(), - })), - isCancel: vi.fn(() => false), - select: vi.fn(), - text: vi.fn(), -})); - -vi.mock("@inquirer/prompts", () => ({ - checkbox: vi.fn(), - confirm: vi.fn(), - input: vi.fn(), - password: vi.fn(), - search: vi.fn(), - select: vi.fn(), -})); - -vi.mock("fs-extra", () => ({ - default: { - readJSONSync: readJSONSyncMock, - writeJSONSync: writeJSONSyncMock, - writeFileSync: writeFileSyncMock, - }, -})); - -vi.mock("execa", () => ({ - execa: execaMock, -})); - -vi.mock("~/helpers/createProject.js", () => ({ - createBareProject: createBareProjectMock, -})); - -vi.mock("~/helpers/setImportAlias.js", () => ({ - setImportAlias: setImportAliasMock, -})); - -vi.mock("~/cli/add/data-source/filemaker.js", () => ({ - promptForFileMakerDataSource: promptForFileMakerDataSourceMock, -})); - -vi.mock("~/generators/fmdapi.js", () => ({ - runCodegenCommand: runCodegenCommandMock, -})); - -vi.mock("~/helpers/git.js", () => ({ - initializeGit: initializeGitMock, -})); - -vi.mock("~/helpers/logNextSteps.js", () => ({ - logNextSteps: logNextStepsMock, -})); - -vi.mock("~/helpers/installDependencies.js", () => ({ - installDependencies: vi.fn(), -})); - -vi.mock("~/generators/auth.js", () => ({ - addAuth: vi.fn(), -})); - -vi.mock("~/installers/index.js", () => ({ - buildPkgInstallerMap: vi.fn(() => ({})), -})); - -vi.mock("~/state.js", () => ({ - state: mockState, - initProgramState: vi.fn(), - isNonInteractiveMode: vi.fn(() => true), -})); - -vi.mock("~/utils/getProofKitVersion.js", () => ({ - getVersion: vi.fn(() => "0.0.0-test"), -})); - -vi.mock("~/utils/getUserPkgManager.js", () => ({ - getUserPkgManager: vi.fn(() => "pnpm"), -})); - -vi.mock("~/utils/parseNameAndPath.js", () => ({ - parseNameAndPath: vi.fn((name: string) => [name, name]), -})); - -vi.mock("~/utils/parseSettings.js", () => ({ - setSettings: vi.fn(), -})); - -vi.mock("~/utils/validateAppName.js", () => ({ - validateAppName: vi.fn(() => undefined), -})); - -vi.mock("~/cli/utils.js", () => ({ - abortIfCancel: vi.fn((value: unknown) => value), -})); - -import { runInit } from "~/cli/init.js"; - -const browserFilemakerFlags = { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - server: undefined, - adminApiKey: undefined, - fileName: "", - layoutName: "", - schemaName: "", - dataApiKey: "", - fmServerURL: "", - auth: "none" as const, - dataSource: "filemaker" as const, - ui: "shadcn" as const, - CI: false, - nonInteractive: true, - tailwind: false, - trpc: false, - prisma: false, - drizzle: false, - appRouter: false, -}; - -describe("runInit browser post-init typegen regression", () => { - beforeEach(() => { - vi.clearAllMocks(); - - mockState.appType = undefined; - mockState.ui = "shadcn"; - mockState.projectDir = "/tmp/proofkit-regression"; - - createBareProjectMock.mockResolvedValue("/tmp/proofkit-regression/demo-browser"); - readJSONSyncMock.mockReturnValue({ name: "placeholder-app" }); - execaMock.mockResolvedValue({ stdout: "9.0.0" }); - promptForFileMakerDataSourceMock.mockResolvedValue(undefined); - - runCodegenCommandMock.mockRejectedValue( - new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ), - ); - }); - - it("does not run initial codegen for browser scaffolds after filemaker setup", async () => { - await expect(runInit("demo-browser", browserFilemakerFlags)).resolves.toBeUndefined(); - - expect(promptForFileMakerDataSourceMock).toHaveBeenCalledWith( - expect.objectContaining({ - projectDir: "/tmp/proofkit-regression/demo-browser", - }), - ); - expect(runCodegenCommandMock).not.toHaveBeenCalled(); - }); - - it("writes pnpm build policy before install for pnpm 10", async () => { - mockState.appType = "webviewer"; - execaMock.mockResolvedValue({ stdout: "10.0.0" }); - - await expect( - runInit("demo-webviewer", { - ...browserFilemakerFlags, - noInstall: false, - dataSource: "none", - }), - ).resolves.toBeUndefined(); - - expect(writeFileSyncMock).toHaveBeenCalledWith( - "/tmp/proofkit-regression/demo-browser/pnpm-workspace.yaml", - expect.stringContaining(' "sharp": false'), - "utf8", - ); - const workspaceWriteCallIndex = writeFileSyncMock.mock.calls.findIndex(([filePath]) => - String(filePath).endsWith("pnpm-workspace.yaml"), - ); - const firstPnpmScriptCallIndex = execaMock.mock.calls.findIndex( - ([command, args]) => command === "pnpm" && Array.isArray(args) && args[0] !== "-v", - ); - expect(workspaceWriteCallIndex).not.toBe(-1); - expect(firstPnpmScriptCallIndex).not.toBe(-1); - const workspaceWriteOrder = writeFileSyncMock.mock.invocationCallOrder[workspaceWriteCallIndex]; - const firstPnpmScriptOrder = execaMock.mock.invocationCallOrder[firstPnpmScriptCallIndex]; - expect(workspaceWriteOrder).toBeDefined(); - expect(firstPnpmScriptOrder).toBeDefined(); - expect(workspaceWriteOrder as number).toBeLessThan(firstPnpmScriptOrder as number); - expect(execaMock).toHaveBeenCalledWith("pnpm", ["fix"], { - cwd: "/tmp/proofkit-regression/demo-browser", - stdio: "pipe", - }); - expect(execaMock).toHaveBeenCalledWith("pnpm", ["lint"], { - cwd: "/tmp/proofkit-regression/demo-browser", - stdio: "pipe", - }); - }); -}); diff --git a/packages/cli/tests/install-fm-addon.test.ts b/packages/cli/tests/install-fm-addon.test.ts index 0d0b83ec..f4ad2c85 100644 --- a/packages/cli/tests/install-fm-addon.test.ts +++ b/packages/cli/tests/install-fm-addon.test.ts @@ -91,7 +91,7 @@ describe("compareAddonVersions", () => { }); describe("getWebViewerAddonMessages", () => { - it("adds an explicit update command when the local add-on is outdated", () => { + it("points to the docs when the local add-on is outdated", () => { const messages = getWebViewerAddonMessages({ hasRequiredLayouts: true, inspection: { @@ -99,7 +99,6 @@ describe("getWebViewerAddonMessages", () => { addonName: "wv", addonDir: "ProofKitWV", addonDisplayName: "ProofKit Web Viewer", - installCommand: "proofkit add addon webviewer", targetDir: "/tmp/ProofKit", installedPath: "/tmp/ProofKit/ProofKit.fmaddon", installedVersion: "2.2.3.0", @@ -108,8 +107,11 @@ describe("getWebViewerAddonMessages", () => { }, }); - expect(messages.warn.join("\n")).toContain("proofkit add addon webviewer"); - expect(messages.nextSteps).toEqual(["proofkit add addon webviewer"]); + expect(messages.warn.join("\n")).toContain("https://proofkit.proof.sh/docs/webviewer"); + expect(messages.nextSteps).toEqual([ + "Update the ProofKit Web Viewer add-on: https://proofkit.proof.sh/docs/webviewer", + ]); + expect(messages.warn.join("\n")).not.toContain("proofkit add addon"); }); }); diff --git a/packages/typegen/package.json b/packages/typegen/package.json index eb4a0ff3..05658122 100644 --- a/packages/typegen/package.json +++ b/packages/typegen/package.json @@ -42,6 +42,12 @@ "default": "./dist/esm/server/app.js" } }, + "./cli": { + "import": { + "types": "./dist/esm/cli.d.ts", + "default": "./dist/esm/cli.js" + } + }, "./src/types.ts": "./src/types.ts", "./package.json": "./package.json" }, diff --git a/packages/typegen/src/cli.ts b/packages/typegen/src/cli.ts index e8a394ee..a173f022 100644 --- a/packages/typegen/src/cli.ts +++ b/packages/typegen/src/cli.ts @@ -217,7 +217,40 @@ program } }); -program.parse(); +/** + * Run the typegen CLI. Used as the package bin entrypoint and re-exported so + * `@proofkit/cli`'s `typegen` command can delegate without duplicating logic. + * + * @param argv user-supplied args (without the node/script prefix). When omitted, + * the process argv is parsed (bin behavior). + */ +export async function runCli(argv?: readonly string[]) { + if (argv === undefined) { + await program.parseAsync(); + return; + } + await program.parseAsync(argv as string[], { from: "user" }); +} + +function isCliEntrypoint() { + const invokedPath = process.argv[1]; + if (!invokedPath) { + return false; + } + const modulePath = fileURLToPath(import.meta.url); + try { + return fs.realpathSync(invokedPath) === fs.realpathSync(modulePath); + } catch { + return path.resolve(invokedPath) === path.resolve(modulePath); + } +} + +if (isCliEntrypoint()) { + runCli().catch((error: unknown) => { + console.error(error); + process.exit(1); + }); +} function parseEnvs(envPath?: string | undefined) { let actualEnvPath = envPath; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d192dd5b..d483d16e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,7 +232,7 @@ importers: version: 0.2.1(@types/node@25.0.6)(rollup@4.55.1)(typescript@5.9.3)(vite@6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) better-auth: specifier: ^1.5.4 - version: 1.5.5(@prisma/client@5.22.0(prisma@5.22.0))(mongodb@7.1.0)(mysql2@3.16.0)(next@16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(prisma@5.22.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.17) + version: 1.5.5(mongodb@7.1.0)(next@16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.17) c12: specifier: ^3.3.3 version: 3.3.3(magicast@0.3.5) @@ -279,18 +279,6 @@ importers: packages/cli: devDependencies: - '@auth/drizzle-adapter': - specifier: ^1.11.1 - version: 1.11.1 - '@auth/prisma-adapter': - specifier: ^1.6.0 - version: 1.6.0(@prisma/client@5.22.0(prisma@5.22.0)) - '@better-fetch/fetch': - specifier: 1.1.17 - version: 1.1.17 - '@clack/core': - specifier: ^0.3.5 - version: 0.3.5 '@clack/prompts': specifier: ^0.11.0 version: 0.11.0 @@ -312,60 +300,15 @@ importers: '@inquirer/prompts': specifier: ^8.3.2 version: 8.3.2(@types/node@22.19.5) - '@libsql/client': - specifier: ^0.6.2 - version: 0.6.2 - '@planetscale/database': - specifier: ^1.19.0 - version: 1.19.0 - '@prisma/adapter-planetscale': - specifier: ^5.22.0 - version: 5.22.0(@planetscale/database@1.19.0) - '@prisma/client': - specifier: ^5.22.0 - version: 5.22.0(prisma@5.22.0) - '@proofkit/better-auth': - specifier: workspace:* - version: link:../better-auth '@proofkit/fmdapi': specifier: workspace:* version: link:../fmdapi '@proofkit/typegen': specifier: workspace:* version: link:../typegen - '@proofkit/webviewer': - specifier: workspace:* - version: link:../webviewer - '@rollup/plugin-replace': - specifier: ^6.0.3 - version: 6.0.3(rollup@4.55.1) - '@t3-oss/env-nextjs': - specifier: ^0.10.1 - version: 0.10.1(typescript@5.9.3)(zod@4.3.5) - '@tanstack/react-query': - specifier: ^5.90.16 - version: 5.90.16(react@19.2.3) - '@trpc/client': - specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441(@trpc/server@11.0.0-rc.441) - '@trpc/next': - specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@trpc/server@11.0.0-rc.441)(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@trpc/react-query': - specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@trpc/server': - specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441 - '@types/axios': - specifier: ^0.14.4 - version: 0.14.4 '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 - '@types/glob': - specifier: ^8.1.0 - version: 8.1.0 '@types/gradient-string': specifier: ^1.1.6 version: 1.1.6 @@ -375,111 +318,39 @@ importers: '@types/randomstring': specifier: ^1.3.0 version: 1.3.0 - '@types/react': - specifier: 19.2.7 - version: 19.2.7 - '@types/semver': - specifier: ^7.7.1 - version: 7.7.1 - '@vitest/coverage-v8': - specifier: ^2.1.9 - version: 2.1.9(vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2)) axios: specifier: ^1.13.2 version: 1.13.2 chalk: specifier: 5.4.1 version: 5.4.1 - commander: - specifier: ^14.0.2 - version: 14.0.2 dotenv: specifier: ^16.6.1 version: 16.6.1 - drizzle-kit: - specifier: ^0.21.4 - version: 0.21.4 - drizzle-orm: - specifier: ^0.30.10 - version: 0.30.10(@libsql/client@0.6.2)(@opentelemetry/api@1.9.1)(@planetscale/database@1.19.0)(@types/react@19.2.7)(kysely@0.28.12)(mysql2@3.16.0)(postgres@3.4.8)(react@19.2.3) effect: specifier: ^3.20.0 version: 3.20.0 - es-toolkit: - specifier: ^1.43.0 - version: 1.43.0 execa: specifier: ^9.6.1 version: 9.6.1 - fast-glob: - specifier: ^3.3.3 - version: 3.3.3 fs-extra: specifier: ^11.3.3 version: 11.3.3 - glob: - specifier: ^11.1.0 - version: 11.1.0 gradient-string: specifier: ^2.0.2 version: 2.0.2 - handlebars: - specifier: ^4.7.8 - version: 4.7.8 - jiti: - specifier: ^1.21.7 - version: 1.21.7 jsonc-parser: specifier: ^3.3.1 version: 3.3.1 - mysql2: - specifier: ^3.16.0 - version: 3.16.0 - next: - specifier: 16.1.1 - version: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0) - next-auth: - specifier: ^4.24.13 - version: 4.24.13(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) open: specifier: ^10.2.0 version: 10.2.0 - ora: - specifier: 6.3.1 - version: 6.3.1 - postgres: - specifier: ^3.4.8 - version: 3.4.8 - prisma: - specifier: ^5.22.0 - version: 5.22.0 publint: specifier: ^0.3.16 version: 0.3.16 randomstring: specifier: ^1.3.1 version: 1.3.1 - react: - specifier: 19.2.3 - version: 19.2.3 - react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) - semver: - specifier: ^7.7.3 - version: 7.7.3 - shadcn: - specifier: ^2.10.0 - version: 2.10.0(@types/node@22.19.5)(hono@4.11.3)(typescript@5.9.3) - superjson: - specifier: ^2.2.6 - version: 2.2.6 - tailwindcss: - specifier: ^4.1.18 - version: 4.1.18 - ts-morph: - specifier: ^26.0.0 - version: 26.0.0 tsdown: specifier: ^0.14.2 version: 0.14.2(oxc-resolver@11.16.2)(publint@0.3.16)(typescript@5.9.3) @@ -491,7 +362,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.3.5 version: 4.3.5 @@ -504,7 +375,7 @@ importers: devDependencies: vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(@vitest/ui@3.2.4)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) packages/fmdapi: dependencies: @@ -760,7 +631,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(@vitest/ui@3.2.4)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) packages/typegen/web: dependencies: @@ -964,42 +835,6 @@ packages: resolution: {integrity: sha512-Izvir8iIoU+X4SKtDAa5kpb+9cpifclzsbA8x/AZY0k0gIfXYQ1fa1B6Epfe6vNA2YfDX8VtrZFgvnXB6aPEoQ==} engines: {node: '>=18'} - '@auth/core@0.29.0': - resolution: {integrity: sha512-MdfEjU6WRjUnPG1+XeBWrTIlAsLZU6V0imCIqVDDDPxLI6UZWldXVqAA2EsDazGofV78jqiCLHaN85mJITDqdg==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - nodemailer: ^6.8.0 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - - '@auth/core@0.41.1': - resolution: {integrity: sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - nodemailer: ^7.0.7 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - - '@auth/drizzle-adapter@1.11.1': - resolution: {integrity: sha512-cQTvDZqsyF7RPhDm/B6SvqdVP9EzQhy3oM4Muu7fjjmSYFLbSR203E6dH631ZHSKDn2b4WZkfMnjPDzRsPSAeA==} - - '@auth/prisma-adapter@1.6.0': - resolution: {integrity: sha512-PQU8/Oi5gfjzb0MkhMGVX0Dg877phPzsQdK54+C7ubukCeZPjyvuSAx1vVtWEYVWp2oQvjgG/C6QiDoeC7S10A==} - peerDependencies: - '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5' - '@aws-crypto/sha256-js@1.2.2': resolution: {integrity: sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==} @@ -1285,9 +1120,6 @@ packages: '@better-auth/utils@0.3.1': resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==} - '@better-fetch/fetch@1.1.17': - resolution: {integrity: sha512-MQonMalbmEshb+amuLtCkVjYliyyWrYXZkiMnHLgFjNEBsNBbZSY3+lYsFK1/VxePSupVkUW6xinqhqB3uHE1g==} - '@better-fetch/fetch@1.1.21': resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} @@ -1412,9 +1244,6 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@clack/core@0.3.5': - resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} - '@clack/core@0.5.0': resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} @@ -1567,20 +1396,6 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild-kit/core-utils@3.3.2': - resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild-kit/esm-loader@2.6.5': - resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild/aix-ppc64@0.19.12': - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1599,18 +1414,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.19.12': - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -1629,18 +1432,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.19.12': - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -1659,18 +1450,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.19.12': - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -1689,18 +1468,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.19.12': - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -1719,18 +1486,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.19.12': - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -1749,18 +1504,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.19.12': - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -1779,18 +1522,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.19.12': - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -1809,18 +1540,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.19.12': - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -1839,18 +1558,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.19.12': - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -1869,18 +1576,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.19.12': - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -1899,18 +1594,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.19.12': - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -1929,18 +1612,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.19.12': - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -1959,18 +1630,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.19.12': - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -1989,18 +1648,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.19.12': - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -2019,18 +1666,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.19.12': - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -2049,18 +1684,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.19.12': - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -2097,18 +1720,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.19.12': - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -2145,18 +1756,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.19.12': - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -2193,18 +1792,6 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.19.12': - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -2223,18 +1810,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.19.12': - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -2253,18 +1828,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.19.12': - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -2283,18 +1846,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.19.12': - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -2764,57 +2315,6 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@libsql/client@0.6.2': - resolution: {integrity: sha512-xRNfRLv/dOCbV4qd+M0baQwGmvuZpMd2wG2UAPs8XmcdaPvu5ErkcaeITkxlm3hDEJVabQM1cFhMBxsugWW9fQ==} - - '@libsql/core@0.6.2': - resolution: {integrity: sha512-c2P4M+4u/4b2L02A0KjggO3UW51rGkhxr/7fzJO0fEAqsqrWGxuNj2YtRkina/oxfYvAof6xjp8RucNoIV/Odw==} - - '@libsql/darwin-arm64@0.3.19': - resolution: {integrity: sha512-rmOqsLcDI65zzxlUOoEiPJLhqmbFsZF6p4UJQ2kMqB+Kc0Rt5/A1OAdOZ/Wo8fQfJWjR1IbkbpEINFioyKf+nQ==} - cpu: [arm64] - os: [darwin] - - '@libsql/darwin-x64@0.3.19': - resolution: {integrity: sha512-q9O55B646zU+644SMmOQL3FIfpmEvdWpRpzubwFc2trsa+zoBlSkHuzU9v/C+UNoPHQVRMP7KQctJ455I/h/xw==} - cpu: [x64] - os: [darwin] - - '@libsql/hrana-client@0.6.2': - resolution: {integrity: sha512-MWxgD7mXLNf9FXXiM0bc90wCjZSpErWKr5mGza7ERy2FJNNMXd7JIOv+DepBA1FQTIfI8TFO4/QDYgaQC0goNw==} - - '@libsql/isomorphic-fetch@0.2.5': - resolution: {integrity: sha512-8s/B2TClEHms2yb+JGpsVRTPBfy1ih/Pq6h6gvyaNcYnMVJvgQRY7wAa8U2nD0dppbCuDU5evTNMEhrQ17ZKKg==} - engines: {node: '>=18.0.0'} - - '@libsql/isomorphic-ws@0.1.5': - resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} - - '@libsql/linux-arm64-gnu@0.3.19': - resolution: {integrity: sha512-mgeAUU1oqqh57k7I3cQyU6Trpdsdt607eFyEmH5QO7dv303ti+LjUvh1pp21QWV6WX7wZyjeJV1/VzEImB+jRg==} - cpu: [arm64] - os: [linux] - - '@libsql/linux-arm64-musl@0.3.19': - resolution: {integrity: sha512-VEZtxghyK6zwGzU9PHohvNxthruSxBEnRrX7BSL5jQ62tN4n2JNepJ6SdzXp70pdzTfwroOj/eMwiPt94gkVRg==} - cpu: [arm64] - os: [linux] - - '@libsql/linux-x64-gnu@0.3.19': - resolution: {integrity: sha512-2t/J7LD5w2f63wGihEO+0GxfTyYIyLGEvTFEsMO16XI5o7IS9vcSHrxsvAJs4w2Pf907uDjmc7fUfMg6L82BrQ==} - cpu: [x64] - os: [linux] - - '@libsql/linux-x64-musl@0.3.19': - resolution: {integrity: sha512-BLsXyJaL8gZD8+3W2LU08lDEd9MIgGds0yPy5iNPp8tfhXx3pV/Fge2GErN0FC+nzt4DYQtjL+A9GUMglQefXQ==} - cpu: [x64] - os: [linux] - - '@libsql/win32-x64-msvc@0.3.19': - resolution: {integrity: sha512-ay1X9AobE4BpzG0XPw1gplyLZPGHIgJOovvW23gUrukRegiUP62uzhpRbKNogLlUOynyXeq//prHgPXiebUfWg==} - cpu: [x64] - os: [win32] - '@loaderkit/resolve@1.0.4': resolution: {integrity: sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A==} @@ -2909,9 +2409,6 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@neon-rs/load@0.0.4': - resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@next/swc-darwin-arm64@16.1.1': resolution: {integrity: sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==} engines: {node: '>= 10'} @@ -3121,8 +2618,8 @@ packages: resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} engines: {node: '>= 20.0.0'} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.134.0': + resolution: {integrity: sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==} '@oxc-resolver/binding-android-arm-eabi@11.16.2': resolution: {integrity: sha512-lVJbvydLQIDZHKUb6Zs9Rq80QVTQ9xdCQE30eC9/cjg4wsMoEOg65QZPymUAIVJotpUAWJD0XYcwE7ugfxx5kQ==} @@ -3276,9 +2773,6 @@ packages: cpu: [x64] os: [win32] - '@panva/hkdf@1.2.1': - resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} - '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -3367,10 +2861,6 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} - '@planetscale/database@1.19.0': - resolution: {integrity: sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA==} - engines: {node: '>=16'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -3380,38 +2870,6 @@ packages: '@posthog/types@1.372.9': resolution: {integrity: sha512-B7k9S+H9WUKHXxe1HOkQWbpWtMcrBvsodm5stZaLQ3pYxf9TowtwssdzTtX4hHjzSYqgrS1IpNnJX4vs1KgBzA==} - '@prisma/adapter-planetscale@5.22.0': - resolution: {integrity: sha512-4fffELMJCAsvLaO4E4YKw6SsX8z3524f0th8dgagr4/p4PQwOJa8wQUktC3DXZdUGG0jyQUZF9ZYPM5e18UB+A==} - peerDependencies: - '@planetscale/database': ^1.15.0 - - '@prisma/client@5.22.0': - resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} - engines: {node: '>=16.13'} - peerDependencies: - prisma: '*' - peerDependenciesMeta: - prisma: - optional: true - - '@prisma/debug@5.22.0': - resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} - - '@prisma/driver-adapter-utils@5.22.0': - resolution: {integrity: sha512-Y8msGZl9unmVflXoqdxTejv99UD02Gp4VoIvkyw+YxNUIj7nRz35O7yf5D87qNmTiPMGCS1WjUucG9ZuNq8+tw==} - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': - resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} - - '@prisma/engines@5.22.0': - resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} - - '@prisma/fetch-engine@5.22.0': - resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} - - '@prisma/get-platform@5.22.0': - resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} - '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -4179,97 +3637,97 @@ packages: peerDependencies: react: '>=18.2.0' - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.0': + resolution: {integrity: sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.0': + resolution: {integrity: sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.0': + resolution: {integrity: sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.0': + resolution: {integrity: sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.0': + resolution: {integrity: sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.0': + resolution: {integrity: sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.0': + resolution: {integrity: sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.0': + resolution: {integrity: sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.0': + resolution: {integrity: sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4280,15 +3738,6 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rollup/plugin-replace@6.0.3': - resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -4550,24 +3999,6 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@t3-oss/env-core@0.10.1': - resolution: {integrity: sha512-GcKZiCfWks5CTxhezn9k5zWX3sMDIYf6Kaxy2Gx9YEQftFcz8hDRN56hcbylyAO3t4jQnQ5ifLawINsNgCDpOg==} - peerDependencies: - typescript: '>=5.0.0' - zod: ^3.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@t3-oss/env-nextjs@0.10.1': - resolution: {integrity: sha512-iy2qqJLnFh1RjEWno2ZeyTu0ufomkXruUsOZludzDIroUabVvHsrSjtkHqwHp1/pgPUzN3yBRHMILW162X7x2Q==} - peerDependencies: - typescript: '>=5.0.0' - zod: ^3.0.0 - peerDependenciesMeta: - typescript: - optional: true - '@tabler/icons-react@3.36.1': resolution: {integrity: sha512-/8nOXeNeMoze9xY/QyEKG65wuvRhkT3q9aytaur6Gj8bYU2A98YVJyLc9MRmc5nVvpy+bRlrrwK/Ykr8WGyUWg==} peerDependencies: @@ -4769,39 +4200,6 @@ packages: '@textlint/utils@15.5.2': resolution: {integrity: sha512-g7Zs8QDZJspno9C7i5iGdwuJ07SxCHWIgxpAebwX503sup4w5IOo3q0X/LxRdl1F4sSAFw/m2glYZomOieNPCw==} - '@trpc/client@11.0.0-rc.441': - resolution: {integrity: sha512-O9zHP7JcK35jO5G8BoW304WdRcHW1TKZae2QDU65KvfMxosbmqY2ajwAgs6CxTS45c1PuF9vI0kXtP52e3FYgQ==} - peerDependencies: - '@trpc/server': 11.0.0-rc.441+0c4a58144 - - '@trpc/next@11.0.0-rc.441': - resolution: {integrity: sha512-C8x7mK2jD+am+vYcFQz5uIRuJFL3gcZ6AyWdZKvI0J6lzd607LXN20V2dgIutclHBj3zOjfWMLRnKilH67JRFw==} - peerDependencies: - '@tanstack/react-query': ^5.49.2 - '@trpc/client': 11.0.0-rc.441+0c4a58144 - '@trpc/react-query': 11.0.0-rc.441+0c4a58144 - '@trpc/server': 11.0.0-rc.441+0c4a58144 - next: '*' - react: '>=16.8.0' - react-dom: '>=16.8.0' - peerDependenciesMeta: - '@tanstack/react-query': - optional: true - '@trpc/react-query': - optional: true - - '@trpc/react-query@11.0.0-rc.441': - resolution: {integrity: sha512-VZm17FyQ/imz5S2pdJe6Qt9Od3JH1jDL8SlI5LZJ/ZXm+vdIbY3KJO1GOBKWfUw5oewaAE/QZJS0xVZqpIvw7g==} - peerDependencies: - '@tanstack/react-query': ^5.49.2 - '@trpc/client': 11.0.0-rc.441+0c4a58144 - '@trpc/server': 11.0.0-rc.441+0c4a58144 - react: '>=18.2.0' - react-dom: '>=18.2.0' - - '@trpc/server@11.0.0-rc.441': - resolution: {integrity: sha512-H0NN85JDgDlvG9tHW9efygLJZbVkszLagm5VeLD8MuhXqqKU+WyMTqb4D8rI560dse4dMC3lI5IoXaCEXMoznA==} - '@trpc/server@11.8.1': resolution: {integrity: sha512-P4rzZRpEL7zDFgjxK65IdyH0e41FMFfTkQkuq0BA5tKcr7E6v9/v38DEklCpoDN6sPiB1Sigy/PUEzHENhswDA==} peerDependencies: @@ -4852,10 +4250,6 @@ packages: '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} - '@types/axios@0.14.4': - resolution: {integrity: sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==} - deprecated: This is a stub types definition. axios provides its own type definitions, so you do not need this installed. - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4871,9 +4265,6 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -4892,9 +4283,6 @@ packages: '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} - '@types/glob@8.1.0': - resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} - '@types/gradient-string@1.1.6': resolution: {integrity: sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==} @@ -4925,9 +4313,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/minimatch@5.1.2': - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -5258,10 +4643,6 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - aws-ssl-profiles@1.1.2: - resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} - engines: {node: '>= 6.0.0'} - axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} @@ -5399,9 +4780,6 @@ packages: resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==} engines: {node: '>=20.19.0'} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@4.9.2: resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} @@ -5529,10 +4907,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - cli-color@2.0.4: - resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} - engines: {node: '>=0.10'} - cli-cursor@4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5629,10 +5003,6 @@ packages: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -5675,10 +5045,6 @@ packages: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -5687,10 +5053,6 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - copy-anything@4.0.5: - resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} - engines: {node: '>=18'} - core-js@3.49.0: resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} @@ -5722,10 +5084,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - d@1.0.2: - resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} - engines: {node: '>=0.12'} - data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -5782,10 +5140,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -5801,10 +5155,6 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} - detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} - engines: {node: '>=8'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -5829,113 +5179,22 @@ packages: diff@8.0.3: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} - engines: {node: '>=0.3.1'} - - difflib@0.2.4: - resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - dompurify@3.4.2: - resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==} - - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} - engines: {node: '>=12'} - - dreamopt@0.8.0: - resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==} - engines: {node: '>=0.4.0'} - - drizzle-kit@0.21.4: - resolution: {integrity: sha512-Nxcc1ONJLRgbhmR+azxjNF9Ly9privNLEIgW53c92whb4xp8jZLH1kMCh/54ci1mTMuYxPdOukqLwJ8wRudNwA==} - hasBin: true - - drizzle-orm@0.30.10: - resolution: {integrity: sha512-IRy/QmMWw9lAQHpwbUh1b8fcn27S/a9zMIzqea1WNOxK9/4EB8gIo+FZWLiPXzl2n9ixGSv8BhsLZiOppWEwBw==} - peerDependencies: - '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=3' - '@electric-sql/pglite': '>=0.1.1' - '@libsql/client': '*' - '@neondatabase/serverless': '>=0.1' - '@op-engineering/op-sqlite': '>=2' - '@opentelemetry/api': ^1.4.1 - '@planetscale/database': '>=1' - '@types/better-sqlite3': '*' - '@types/pg': '*' - '@types/react': '>=18' - '@types/sql.js': '*' - '@vercel/postgres': '>=0.8.0' - '@xata.io/client': '*' - better-sqlite3: '>=7' - bun-types: '*' - expo-sqlite: '>=13.2.0' - knex: '*' - kysely: '*' - mysql2: '>=2' - pg: '>=8' - postgres: '>=3' - react: '>=18' - sql.js: '>=1' - sqlite3: '>=5' - peerDependenciesMeta: - '@aws-sdk/client-rds-data': - optional: true - '@cloudflare/workers-types': - optional: true - '@electric-sql/pglite': - optional: true - '@libsql/client': - optional: true - '@neondatabase/serverless': - optional: true - '@op-engineering/op-sqlite': - optional: true - '@opentelemetry/api': - optional: true - '@planetscale/database': - optional: true - '@types/better-sqlite3': - optional: true - '@types/pg': - optional: true - '@types/react': - optional: true - '@types/sql.js': - optional: true - '@vercel/postgres': - optional: true - '@xata.io/client': - optional: true - better-sqlite3: - optional: true - bun-types: - optional: true - expo-sqlite: - optional: true - knex: - optional: true - kysely: - optional: true - mysql2: - optional: true - pg: - optional: true - postgres: - optional: true - react: - optional: true - sql.js: - optional: true - sqlite3: - optional: true + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dompurify@3.4.2: + resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} dts-resolver@2.1.3: resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} @@ -6004,10 +5263,6 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} - env-paths@3.0.0: - resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -6037,41 +5292,12 @@ packages: es-toolkit@1.43.0: resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} - es5-ext@0.10.64: - resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} - engines: {node: '>=0.10'} - - es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - - es6-symbol@3.1.4: - resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} - engines: {node: '>=0.12'} - - es6-weak-map@2.0.3: - resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} - esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - esbuild-register@3.6.0: - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -6106,10 +5332,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - esniff@2.0.1: - resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} - engines: {node: '>=0.10'} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -6146,9 +5368,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-emitter@0.3.5: - resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -6189,9 +5408,6 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -6471,9 +5687,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - generate-function@2.3.1: - resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6552,14 +5765,6 @@ packages: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - - hanji@0.0.5: - resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} - happy-dom@20.1.0: resolution: {integrity: sha512-ebvqjBqzenBk2LjzNEAzoj7yhw7rW/R2/wVevMu6Mrq3MXtcI/RUz4+ozpcOcqVLEWPqLfg2v9EAU7fFXZUUJw==} engines: {node: '>=20.0.0'} @@ -6610,9 +5815,6 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} - heap@0.2.7: - resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} - highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -6804,15 +6006,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-promise@2.2.2: - resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-property@1.0.2: - resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} - is-regexp@3.1.0: resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} engines: {node: '>=12'} @@ -6837,10 +6033,6 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - is-what@5.5.0: - resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} - engines: {node: '>=18'} - is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -6909,18 +6101,9 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - jose@4.15.9: - resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} - - jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - js-base64@3.7.8: - resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} - js-cookie@2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} @@ -6940,10 +6123,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-diff@0.9.0: - resolution: {integrity: sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==} - hasBin: true - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -7000,11 +6179,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libsql@0.3.19: - resolution: {integrity: sha512-Aj5cQ5uk/6fHdmeW0TiXK42FqUlwx7ytmMLPSaUQPin5HKKKuUPD62MAbN4OEweGBBI7q1BekoEN4gPUEL6MZA==} - cpu: [x64, arm64, wasm32] - os: [darwin, linux, win32] - lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -7159,13 +6333,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lru-queue@0.1.0: - resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} - - lru.min@1.1.3: - resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} - engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - lucide-react@0.511.0: resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} peerDependencies: @@ -7299,10 +6466,6 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - memoizee@0.4.17: - resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} - engines: {node: '>=0.12'} - memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} @@ -7596,17 +6759,9 @@ packages: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} - mysql2@3.16.0: - resolution: {integrity: sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==} - engines: {node: '>= 8.0'} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - named-placeholders@1.1.6: - resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} - engines: {node: '>=8.0.0'} - nano-spawn@2.0.0: resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} engines: {node: '>=20.17'} @@ -7624,36 +6779,16 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} - next-auth@4.24.13: - resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==} - peerDependencies: - '@auth/core': 0.34.3 - next: ^12.2.5 || ^13 || ^14 || ^15 || ^16 - nodemailer: ^7.0.7 - react: ^17.0.2 || ^18 || ^19 - react-dom: ^17.0.2 || ^18 || ^19 - peerDependenciesMeta: - '@auth/core': - optional: true - nodemailer: - optional: true - next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - next@16.1.1: resolution: {integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==} engines: {node: '>=20.9.0'} @@ -7761,23 +6896,10 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true - oauth4webapi@2.17.0: - resolution: {integrity: sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==} - - oauth4webapi@3.8.3: - resolution: {integrity: sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==} - - oauth@0.9.15: - resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -7794,10 +6916,6 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - oidc-token-hash@5.2.0: - resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} - engines: {node: ^10.13.0 || >=12.0.0} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -7827,9 +6945,6 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} - openid-client@5.7.1: - resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -8015,28 +7130,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - postgres@3.4.8: - resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} - engines: {node: '>=12'} - posthog-js@1.372.9: resolution: {integrity: sha512-qFhTxxrONCO4YBubuEp6/f3beRPku8OqgfHwpSro/XcDU0oPXMVyvsXGUnxhjaq4PvQb79PwvquAX0/HIGcnWg==} - preact-render-to-string@5.2.3: - resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} - peerDependencies: - preact: ^10.26.10 - - preact-render-to-string@5.2.6: - resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} - peerDependencies: - preact: ^10.26.10 - - preact-render-to-string@6.5.11: - resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} - peerDependencies: - preact: ^10.26.10 - preact@10.28.2: resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} @@ -8058,18 +7154,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-format@3.8.0: - resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} - prisma@5.22.0: - resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} - engines: {node: '>=16.13'} - hasBin: true - prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -8371,8 +7459,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.0: + resolution: {integrity: sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -8437,9 +7525,6 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - seq-queue@0.0.5: - resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -8543,9 +7628,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -8581,10 +7663,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sqlstring@2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} - stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -8697,10 +7775,6 @@ packages: babel-plugin-macros: optional: true - superjson@2.2.6: - resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} - engines: {node: '>=16'} - supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -8765,10 +7839,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - timers-ext@0.1.8: - resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} - engines: {node: '>=0.12'} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -8970,9 +8040,6 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - type@2.7.3: - resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} - typescript@5.4.2: resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} @@ -8996,11 +8063,6 @@ packages: ufo@1.6.2: resolution: {integrity: sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==} - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - ultracite@7.0.8: resolution: {integrity: sha512-b98lKaVl3UtH1TF6gZjhPQgtx063i0XpdV1nHEfexHsLyLaaosqU9FT8Tw/HwQkb/UmJ8WihKndur0bSUT0BYw==} hasBin: true @@ -9137,11 +8199,6 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -9333,9 +8390,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} @@ -9477,41 +8531,6 @@ snapshots: typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 - '@auth/core@0.29.0': - dependencies: - '@panva/hkdf': 1.2.1 - '@types/cookie': 0.6.0 - cookie: 0.6.0 - jose: 5.10.0 - oauth4webapi: 2.17.0 - preact: 10.28.2 - preact-render-to-string: 5.2.3(preact@10.28.2) - - '@auth/core@0.41.1': - dependencies: - '@panva/hkdf': 1.2.1 - jose: 6.1.3 - oauth4webapi: 3.8.3 - preact: 10.28.2 - preact-render-to-string: 6.5.11(preact@10.28.2) - - '@auth/drizzle-adapter@1.11.1': - dependencies: - '@auth/core': 0.41.1 - transitivePeerDependencies: - - '@simplewebauthn/browser' - - '@simplewebauthn/server' - - nodemailer - - '@auth/prisma-adapter@1.6.0(@prisma/client@5.22.0(prisma@5.22.0))': - dependencies: - '@auth/core': 0.29.0 - '@prisma/client': 5.22.0(prisma@5.22.0) - transitivePeerDependencies: - - '@simplewebauthn/browser' - - '@simplewebauthn/server' - - nodemailer - '@aws-crypto/sha256-js@1.2.2': dependencies: '@aws-crypto/util': 1.2.2 @@ -9838,13 +8857,10 @@ snapshots: '@better-auth/utils': 0.3.1 mongodb: 7.1.0 - '@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0)': + '@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': dependencies: '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - optionalDependencies: - '@prisma/client': 5.22.0(prisma@5.22.0) - prisma: 5.22.0 '@better-auth/telemetry@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))': dependencies: @@ -9854,8 +8870,6 @@ snapshots: '@better-auth/utils@0.3.1': {} - '@better-fetch/fetch@1.1.17': {} - '@better-fetch/fetch@1.1.21': {} '@biomejs/biome@2.3.11': @@ -10051,11 +9065,6 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@clack/core@0.3.5': - dependencies: - picocolors: 1.1.1 - sisteransi: 1.0.5 - '@clack/core@0.5.0': dependencies: picocolors: 1.1.1 @@ -10234,19 +9243,6 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild-kit/core-utils@3.3.2': - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - - '@esbuild-kit/esm-loader@2.6.5': - dependencies: - '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.13.0 - - '@esbuild/aix-ppc64@0.19.12': - optional: true - '@esbuild/aix-ppc64@0.25.12': optional: true @@ -10256,12 +9252,6 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/android-arm64@0.18.20': - optional: true - - '@esbuild/android-arm64@0.19.12': - optional: true - '@esbuild/android-arm64@0.25.12': optional: true @@ -10271,12 +9261,6 @@ snapshots: '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm@0.18.20': - optional: true - - '@esbuild/android-arm@0.19.12': - optional: true - '@esbuild/android-arm@0.25.12': optional: true @@ -10286,12 +9270,6 @@ snapshots: '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-x64@0.18.20': - optional: true - - '@esbuild/android-x64@0.19.12': - optional: true - '@esbuild/android-x64@0.25.12': optional: true @@ -10301,12 +9279,6 @@ snapshots: '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.18.20': - optional: true - - '@esbuild/darwin-arm64@0.19.12': - optional: true - '@esbuild/darwin-arm64@0.25.12': optional: true @@ -10316,12 +9288,6 @@ snapshots: '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-x64@0.18.20': - optional: true - - '@esbuild/darwin-x64@0.19.12': - optional: true - '@esbuild/darwin-x64@0.25.12': optional: true @@ -10331,12 +9297,6 @@ snapshots: '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.18.20': - optional: true - - '@esbuild/freebsd-arm64@0.19.12': - optional: true - '@esbuild/freebsd-arm64@0.25.12': optional: true @@ -10346,12 +9306,6 @@ snapshots: '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.18.20': - optional: true - - '@esbuild/freebsd-x64@0.19.12': - optional: true - '@esbuild/freebsd-x64@0.25.12': optional: true @@ -10361,12 +9315,6 @@ snapshots: '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-arm64@0.18.20': - optional: true - - '@esbuild/linux-arm64@0.19.12': - optional: true - '@esbuild/linux-arm64@0.25.12': optional: true @@ -10376,12 +9324,6 @@ snapshots: '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm@0.18.20': - optional: true - - '@esbuild/linux-arm@0.19.12': - optional: true - '@esbuild/linux-arm@0.25.12': optional: true @@ -10391,12 +9333,6 @@ snapshots: '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-ia32@0.18.20': - optional: true - - '@esbuild/linux-ia32@0.19.12': - optional: true - '@esbuild/linux-ia32@0.25.12': optional: true @@ -10406,12 +9342,6 @@ snapshots: '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-loong64@0.18.20': - optional: true - - '@esbuild/linux-loong64@0.19.12': - optional: true - '@esbuild/linux-loong64@0.25.12': optional: true @@ -10421,12 +9351,6 @@ snapshots: '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-mips64el@0.18.20': - optional: true - - '@esbuild/linux-mips64el@0.19.12': - optional: true - '@esbuild/linux-mips64el@0.25.12': optional: true @@ -10436,12 +9360,6 @@ snapshots: '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-ppc64@0.18.20': - optional: true - - '@esbuild/linux-ppc64@0.19.12': - optional: true - '@esbuild/linux-ppc64@0.25.12': optional: true @@ -10451,12 +9369,6 @@ snapshots: '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.18.20': - optional: true - - '@esbuild/linux-riscv64@0.19.12': - optional: true - '@esbuild/linux-riscv64@0.25.12': optional: true @@ -10466,12 +9378,6 @@ snapshots: '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-s390x@0.18.20': - optional: true - - '@esbuild/linux-s390x@0.19.12': - optional: true - '@esbuild/linux-s390x@0.25.12': optional: true @@ -10481,12 +9387,6 @@ snapshots: '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-x64@0.18.20': - optional: true - - '@esbuild/linux-x64@0.19.12': - optional: true - '@esbuild/linux-x64@0.25.12': optional: true @@ -10505,12 +9405,6 @@ snapshots: '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.18.20': - optional: true - - '@esbuild/netbsd-x64@0.19.12': - optional: true - '@esbuild/netbsd-x64@0.25.12': optional: true @@ -10529,12 +9423,6 @@ snapshots: '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.18.20': - optional: true - - '@esbuild/openbsd-x64@0.19.12': - optional: true - '@esbuild/openbsd-x64@0.25.12': optional: true @@ -10553,12 +9441,6 @@ snapshots: '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/sunos-x64@0.18.20': - optional: true - - '@esbuild/sunos-x64@0.19.12': - optional: true - '@esbuild/sunos-x64@0.25.12': optional: true @@ -10568,12 +9450,6 @@ snapshots: '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/win32-arm64@0.18.20': - optional: true - - '@esbuild/win32-arm64@0.19.12': - optional: true - '@esbuild/win32-arm64@0.25.12': optional: true @@ -10583,12 +9459,6 @@ snapshots: '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-ia32@0.18.20': - optional: true - - '@esbuild/win32-ia32@0.19.12': - optional: true - '@esbuild/win32-ia32@0.25.12': optional: true @@ -10598,12 +9468,6 @@ snapshots: '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-x64@0.18.20': - optional: true - - '@esbuild/win32-x64@0.19.12': - optional: true - '@esbuild/win32-x64@0.25.12': optional: true @@ -10772,7 +9636,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.8.1 + '@emnapi/runtime': 1.10.0 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -11013,73 +9877,18 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@keyv/bigmap@1.3.1(keyv@5.6.0)': - dependencies: - hashery: 1.5.0 - hookified: 1.15.1 - keyv: 5.6.0 - - '@keyv/serialize@1.1.1': {} - - '@libsql/client@0.6.2': - dependencies: - '@libsql/core': 0.6.2 - '@libsql/hrana-client': 0.6.2 - js-base64: 3.7.8 - libsql: 0.3.19 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/core@0.6.2': - dependencies: - js-base64: 3.7.8 - - '@libsql/darwin-arm64@0.3.19': - optional: true - - '@libsql/darwin-x64@0.3.19': - optional: true - - '@libsql/hrana-client@0.6.2': - dependencies: - '@libsql/isomorphic-fetch': 0.2.5 - '@libsql/isomorphic-ws': 0.1.5 - js-base64: 3.7.8 - node-fetch: 3.3.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/isomorphic-fetch@0.2.5': {} - - '@libsql/isomorphic-ws@0.1.5': - dependencies: - '@types/ws': 8.18.1 - ws: 8.19.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/linux-arm64-gnu@0.3.19': - optional: true - - '@libsql/linux-arm64-musl@0.3.19': - optional: true - - '@libsql/linux-x64-gnu@0.3.19': - optional: true + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 - '@libsql/linux-x64-musl@0.3.19': - optional: true + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.0 + hookified: 1.15.1 + keyv: 5.6.0 - '@libsql/win32-x64-msvc@0.3.19': - optional: true + '@keyv/serialize@1.1.1': {} '@loaderkit/resolve@1.0.4': dependencies: @@ -11295,8 +10104,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@neon-rs/load@0.0.4': {} - '@next/swc-darwin-arm64@16.1.1': optional: true @@ -11448,7 +10255,7 @@ snapshots: '@orama/orama@3.1.18': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.134.0': {} '@oxc-resolver/binding-android-arm-eabi@11.16.2': optional: true @@ -11536,8 +10343,6 @@ snapshots: '@oxlint/win32-x64@1.39.0': optional: true - '@panva/hkdf@1.2.1': {} - '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -11598,8 +10403,6 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6 - '@planetscale/database@1.19.0': {} - '@polka/url@1.0.0-next.29': {} '@posthog/core@1.28.3': @@ -11608,40 +10411,6 @@ snapshots: '@posthog/types@1.372.9': {} - '@prisma/adapter-planetscale@5.22.0(@planetscale/database@1.19.0)': - dependencies: - '@planetscale/database': 1.19.0 - '@prisma/driver-adapter-utils': 5.22.0 - - '@prisma/client@5.22.0(prisma@5.22.0)': - optionalDependencies: - prisma: 5.22.0 - - '@prisma/debug@5.22.0': {} - - '@prisma/driver-adapter-utils@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} - - '@prisma/engines@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/fetch-engine': 5.22.0 - '@prisma/get-platform': 5.22.0 - - '@prisma/fetch-engine@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/get-platform': 5.22.0 - - '@prisma/get-platform@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -12447,66 +11216,59 @@ snapshots: dependencies: react: 19.2.3 - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.0': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.0': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.0': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.0': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.0': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.0': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.0': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.0': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.0': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.0': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.0': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.0': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.0': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.0': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.0': optional: true '@rolldown/pluginutils@1.0.0': {} '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/plugin-replace@6.0.3(rollup@4.55.1)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.55.1) - magic-string: 0.30.21 - optionalDependencies: - rollup: 4.55.1 - '@rollup/pluginutils@5.3.0(rollup@4.55.1)': dependencies: '@types/estree': 1.0.8 @@ -12775,19 +11537,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.10.1(typescript@5.9.3)(zod@4.3.5)': - dependencies: - zod: 4.3.5 - optionalDependencies: - typescript: 5.9.3 - - '@t3-oss/env-nextjs@0.10.1(typescript@5.9.3)(zod@4.3.5)': - dependencies: - '@t3-oss/env-core': 0.10.1(typescript@5.9.3)(zod@4.3.5) - zod: 4.3.5 - optionalDependencies: - typescript: 5.9.3 - '@tabler/icons-react@3.36.1(react@19.2.3)': dependencies: '@tabler/icons': 3.36.1 @@ -13055,31 +11804,6 @@ snapshots: '@textlint/utils@15.5.2': {} - '@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441)': - dependencies: - '@trpc/server': 11.0.0-rc.441 - - '@trpc/next@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@trpc/server@11.0.0-rc.441)(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@trpc/client': 11.0.0-rc.441(@trpc/server@11.0.0-rc.441) - '@trpc/server': 11.0.0-rc.441 - next: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - optionalDependencies: - '@tanstack/react-query': 5.90.16(react@19.2.3) - '@trpc/react-query': 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - - '@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/react-query': 5.90.16(react@19.2.3) - '@trpc/client': 11.0.0-rc.441(@trpc/server@11.0.0-rc.441) - '@trpc/server': 11.0.0-rc.441 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - - '@trpc/server@11.0.0-rc.441': {} - '@trpc/server@11.8.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -13128,12 +11852,6 @@ snapshots: '@types/argparse@1.0.38': {} - '@types/axios@0.14.4': - dependencies: - axios: 1.13.2 - transitivePeerDependencies: - - debug - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -13160,8 +11878,6 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/cookie@0.6.0': {} - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -13181,11 +11897,6 @@ snapshots: '@types/jsonfile': 6.1.4 '@types/node': 25.0.6 - '@types/glob@8.1.0': - dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 22.19.5 - '@types/gradient-string@1.1.6': dependencies: '@types/tinycolor2': 1.4.6 @@ -13223,8 +11934,6 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/minimatch@5.1.2': {} - '@types/ms@2.1.0': {} '@types/node@12.20.55': {} @@ -13337,24 +12046,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.9(vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 - debug: 4.4.3(supports-color@5.5.0) - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.2.0 - magic-string: 0.30.21 - magicast: 0.3.5 - std-env: 3.10.0 - test-exclude: 7.0.1 - tinyrainbow: 1.2.0 - vitest: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - supports-color - '@vitest/coverage-v8@2.1.9(vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 @@ -13400,14 +12091,14 @@ snapshots: msw: 2.12.7(@types/node@22.19.5)(typescript@5.9.3) vite: 6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@25.0.6)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.7(@types/node@25.0.6)(typescript@5.9.3) - vite: 6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.0.6)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': dependencies: @@ -13639,8 +12330,6 @@ snapshots: asynckit@0.4.0: {} - aws-ssl-profiles@1.1.2: {} - axios@1.13.2: dependencies: follow-redirects: 1.15.11 @@ -13666,14 +12355,14 @@ snapshots: elkjs: 0.11.1 entities: 7.0.1 - better-auth@1.5.5(@prisma/client@5.22.0(prisma@5.22.0))(mongodb@7.1.0)(mysql2@3.16.0)(next@16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(prisma@5.22.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.17): + better-auth@1.5.5(mongodb@7.1.0)(next@16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.17): dependencies: '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1) '@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1) '@better-auth/kysely-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.12) '@better-auth/memory-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1) '@better-auth/mongo-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0) + '@better-auth/prisma-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1) '@better-auth/telemetry': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -13686,11 +12375,8 @@ snapshots: nanostores: 1.1.1 zod: 4.3.6 optionalDependencies: - '@prisma/client': 5.22.0(prisma@5.22.0) mongodb: 7.1.0 - mysql2: 3.16.0 next: 16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0) - prisma: 5.22.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) vitest: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(@vitest/ui@3.2.4)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) @@ -13759,8 +12445,6 @@ snapshots: bson@7.2.0: {} - buffer-from@1.1.2: {} - buffer@4.9.2: dependencies: base64-js: 1.5.1 @@ -13893,14 +12577,6 @@ snapshots: dependencies: clsx: 2.1.1 - cli-color@2.0.4: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-iterator: 2.0.3 - memoizee: 0.4.17 - timers-ext: 0.1.8 - cli-cursor@4.0.0: dependencies: restore-cursor: 4.0.0 @@ -13999,8 +12675,6 @@ snapshots: commander@14.0.2: {} - commander@9.5.0: {} - compare-versions@6.1.1: {} compute-scroll-into-view@3.1.1: {} @@ -14035,16 +12709,10 @@ snapshots: cookie-signature@1.2.2: {} - cookie@0.6.0: {} - cookie@0.7.2: {} cookie@1.1.1: {} - copy-anything@4.0.5: - dependencies: - is-what: 5.5.0 - core-js@3.49.0: {} cors@2.8.5: @@ -14073,11 +12741,6 @@ snapshots: csstype@3.2.3: {} - d@1.0.2: - dependencies: - es5-ext: 0.10.64 - type: 2.7.3 - data-uri-to-buffer@4.0.1: {} date-fns@2.30.0: @@ -14119,8 +12782,6 @@ snapshots: delayed-stream@1.0.0: {} - denque@2.1.0: {} - depd@2.0.0: {} dequal@2.0.3: {} @@ -14129,8 +12790,6 @@ snapshots: detect-indent@6.1.0: {} - detect-libc@2.0.2: {} - detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -14147,10 +12806,6 @@ snapshots: diff@8.0.3: {} - difflib@0.2.4: - dependencies: - heap: 0.2.7 - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -14163,35 +12818,6 @@ snapshots: dotenv@17.2.3: {} - dreamopt@0.8.0: - dependencies: - wordwrap: 1.0.0 - - drizzle-kit@0.21.4: - dependencies: - '@esbuild-kit/esm-loader': 2.6.5 - commander: 9.5.0 - env-paths: 3.0.0 - esbuild: 0.19.12 - esbuild-register: 3.6.0(esbuild@0.19.12) - glob: 11.1.0 - hanji: 0.0.5 - json-diff: 0.9.0 - zod: 3.25.76 - transitivePeerDependencies: - - supports-color - - drizzle-orm@0.30.10(@libsql/client@0.6.2)(@opentelemetry/api@1.9.1)(@planetscale/database@1.19.0)(@types/react@19.2.7)(kysely@0.28.12)(mysql2@3.16.0)(postgres@3.4.8)(react@19.2.3): - optionalDependencies: - '@libsql/client': 0.6.2 - '@opentelemetry/api': 1.9.1 - '@planetscale/database': 1.19.0 - '@types/react': 19.2.7 - kysely: 0.28.12 - mysql2: 3.16.0 - postgres: 3.4.8 - react: 19.2.3 - dts-resolver@2.1.3(oxc-resolver@11.16.2): optionalDependencies: oxc-resolver: 11.16.2 @@ -14243,8 +12869,6 @@ snapshots: entities@7.0.1: {} - env-paths@3.0.0: {} - environment@1.1.0: {} error-ex@1.3.4: @@ -14270,31 +12894,6 @@ snapshots: es-toolkit@1.43.0: {} - es5-ext@0.10.64: - dependencies: - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esniff: 2.0.1 - next-tick: 1.1.0 - - es6-iterator@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-symbol: 3.1.4 - - es6-symbol@3.1.4: - dependencies: - d: 1.0.2 - ext: 1.7.0 - - es6-weak-map@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -14309,64 +12908,6 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild-register@3.6.0(esbuild@0.19.12): - dependencies: - debug: 4.4.3(supports-color@5.5.0) - esbuild: 0.19.12 - transitivePeerDependencies: - - supports-color - - esbuild@0.18.20: - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - - esbuild@0.19.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -14464,13 +13005,6 @@ snapshots: escape-string-regexp@5.0.0: {} - esniff@2.0.1: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - event-emitter: 0.3.5 - type: 2.7.3 - esprima@4.0.1: {} estree-util-attach-comments@3.0.0: @@ -14514,11 +13048,6 @@ snapshots: etag@1.8.1: {} - event-emitter@0.3.5: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - eventemitter3@5.0.1: {} eventsource-parser@3.0.6: {} @@ -14603,10 +13132,6 @@ snapshots: exsolve@1.0.8: {} - ext@1.7.0: - dependencies: - type: 2.7.3 - extend@3.0.2: {} extendable-error@0.1.7: {} @@ -14938,10 +13463,6 @@ snapshots: function-bind@1.1.2: {} - generate-function@2.3.1: - dependencies: - is-property: 1.0.2 - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -15027,20 +13548,6 @@ snapshots: graphql@16.12.0: {} - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - - hanji@0.0.5: - dependencies: - lodash.throttle: 4.1.1 - sisteransi: 1.0.5 - happy-dom@20.1.0: dependencies: '@types/node': 20.19.28 @@ -15137,8 +13644,6 @@ snapshots: headers-polyfill@4.0.3: {} - heap@0.2.7: {} - highlight.js@10.7.3: {} hono@4.11.3: {} @@ -15277,12 +13782,8 @@ snapshots: is-plain-obj@4.1.0: {} - is-promise@2.2.2: {} - is-promise@4.0.0: {} - is-property@1.0.2: {} - is-regexp@3.1.0: {} is-stream@3.0.0: {} @@ -15297,8 +13798,6 @@ snapshots: is-unicode-supported@2.1.0: {} - is-what@5.5.0: {} - is-windows@1.0.2: {} is-wsl@3.1.0: @@ -15384,14 +13883,8 @@ snapshots: jju@1.4.0: {} - jose@4.15.9: {} - - jose@5.10.0: {} - jose@6.1.3: {} - js-base64@3.7.8: {} - js-cookie@2.2.1: {} js-tokens@4.0.0: {} @@ -15407,12 +13900,6 @@ snapshots: jsesc@3.1.0: {} - json-diff@0.9.0: - dependencies: - cli-color: 2.0.4 - difflib: 0.2.4 - dreamopt: 0.8.0 - json-parse-even-better-errors@2.3.1: {} json-schema-traverse@1.0.0: {} @@ -15469,19 +13956,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libsql@0.3.19: - dependencies: - '@neon-rs/load': 0.0.4 - detect-libc: 2.0.2 - optionalDependencies: - '@libsql/darwin-arm64': 0.3.19 - '@libsql/darwin-x64': 0.3.19 - '@libsql/linux-arm64-gnu': 0.3.19 - '@libsql/linux-arm64-musl': 0.3.19 - '@libsql/linux-x64-gnu': 0.3.19 - '@libsql/linux-x64-musl': 0.3.19 - '@libsql/win32-x64-msvc': 0.3.19 - lightningcss-android-arm64@1.30.2: optional: true @@ -15617,12 +14091,6 @@ snapshots: dependencies: yallist: 4.0.0 - lru-queue@0.1.0: - dependencies: - es5-ext: 0.10.64 - - lru.min@1.1.3: {} - lucide-react@0.511.0(react@19.2.3): dependencies: react: 19.2.3 @@ -15908,17 +14376,6 @@ snapshots: media-typer@1.1.0: {} - memoizee@0.4.17: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-weak-map: 2.0.3 - event-emitter: 0.3.5 - is-promise: 2.2.2 - lru-queue: 0.1.0 - next-tick: 1.1.0 - timers-ext: 0.1.8 - memory-pager@1.5.0: {} merge-descriptors@2.0.0: {} @@ -16399,28 +14856,12 @@ snapshots: mute-stream@3.0.0: {} - mysql2@3.16.0: - dependencies: - aws-ssl-profiles: 1.1.2 - denque: 2.1.0 - generate-function: 2.3.1 - iconv-lite: 0.7.2 - long: 5.3.2 - lru.min: 1.1.3 - named-placeholders: 1.1.6 - seq-queue: 0.0.5 - sqlstring: 2.3.3 - mz@2.7.0: dependencies: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 - named-placeholders@1.1.6: - dependencies: - lru.min: 1.1.3 - nano-spawn@2.0.0: {} nanoid@3.3.11: {} @@ -16429,32 +14870,13 @@ snapshots: negotiator@1.0.0: {} - neo-async@2.6.2: {} - neotraverse@0.6.18: {} - next-auth@4.24.13(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - '@babel/runtime': 7.28.4 - '@panva/hkdf': 1.2.1 - cookie: 0.7.2 - jose: 4.15.9 - next: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0) - oauth: 0.9.15 - openid-client: 5.7.1 - preact: 10.28.2 - preact-render-to-string: 5.2.6(preact@10.28.2) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - uuid: 8.3.2 - next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next-tick@1.1.0: {} - next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0): dependencies: '@next/env': '@varlock/nextjs-integration@1.1.0(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(varlock@1.2.0)' @@ -16577,16 +14999,8 @@ snapshots: pkg-types: 2.3.0 tinyexec: 1.0.2 - oauth4webapi@2.17.0: {} - - oauth4webapi@3.8.3: {} - - oauth@0.9.15: {} - object-assign@4.1.1: {} - object-hash@2.2.0: {} - object-inspect@1.13.4: {} obug@2.1.1: {} @@ -16599,8 +15013,6 @@ snapshots: ohash@2.0.11: {} - oidc-token-hash@5.2.0: {} - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -16636,13 +15048,6 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - openid-client@5.7.1: - dependencies: - jose: 4.15.9 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.2.0 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -16850,8 +15255,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postgres@3.4.8: {} - posthog-js@1.372.9: dependencies: '@opentelemetry/api': 1.9.1 @@ -16868,20 +15271,6 @@ snapshots: query-selector-shadow-dom: 1.0.1 web-vitals: 5.2.0 - preact-render-to-string@5.2.3(preact@10.28.2): - dependencies: - preact: 10.28.2 - pretty-format: 3.8.0 - - preact-render-to-string@5.2.6(preact@10.28.2): - dependencies: - preact: 10.28.2 - pretty-format: 3.8.0 - - preact-render-to-string@6.5.11(preact@10.28.2): - dependencies: - preact: 10.28.2 - preact@10.28.2: {} prelude-ls@1.2.1: {} @@ -16896,18 +15285,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-format@3.8.0: {} - pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 - prisma@5.22.0: - dependencies: - '@prisma/engines': 5.22.0 - optionalDependencies: - fsevents: 2.3.3 - prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -17310,7 +15691,7 @@ snapshots: rfdc@1.4.1: {} - rolldown-plugin-dts@0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.3)(typescript@5.9.3): + rolldown-plugin-dts@0.15.10(oxc-resolver@11.16.2)(rolldown@1.1.0)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -17320,33 +15701,33 @@ snapshots: debug: 4.4.3(supports-color@5.5.0) dts-resolver: 2.1.3(oxc-resolver@11.16.2) get-tsconfig: 4.13.0 - rolldown: 1.0.3 + rolldown: 1.1.0 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.3: + rolldown@1.1.0: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.134.0 '@rolldown/pluginutils': 1.0.0 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.0 + '@rolldown/binding-darwin-arm64': 1.1.0 + '@rolldown/binding-darwin-x64': 1.1.0 + '@rolldown/binding-freebsd-x64': 1.1.0 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.0 + '@rolldown/binding-linux-arm64-gnu': 1.1.0 + '@rolldown/binding-linux-arm64-musl': 1.1.0 + '@rolldown/binding-linux-ppc64-gnu': 1.1.0 + '@rolldown/binding-linux-s390x-gnu': 1.1.0 + '@rolldown/binding-linux-x64-gnu': 1.1.0 + '@rolldown/binding-linux-x64-musl': 1.1.0 + '@rolldown/binding-openharmony-arm64': 1.1.0 + '@rolldown/binding-wasm32-wasi': 1.1.0 + '@rolldown/binding-win32-arm64-msvc': 1.1.0 + '@rolldown/binding-win32-x64-msvc': 1.1.0 rollup-plugin-preserve-directives@0.4.0(rollup@4.55.1): dependencies: @@ -17445,8 +15826,6 @@ snapshots: transitivePeerDependencies: - supports-color - seq-queue@0.0.5: {} - serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -17619,11 +15998,6 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - source-map@0.6.1: {} source-map@0.7.6: {} @@ -17657,8 +16031,6 @@ snapshots: sprintf-js@1.0.3: {} - sqlstring@2.3.3: {} - stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -17764,10 +16136,6 @@ snapshots: optionalDependencies: '@babel/core': 7.28.5 - superjson@2.2.6: - dependencies: - copy-anything: 4.0.5 - supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -17854,11 +16222,6 @@ snapshots: dependencies: any-promise: 1.3.0 - timers-ext@0.1.8: - dependencies: - es5-ext: 0.10.64 - next-tick: 1.1.0 - tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -17963,8 +16326,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.3 - rolldown-plugin-dts: 0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.3)(typescript@5.9.3) + rolldown: 1.1.0 + rolldown-plugin-dts: 0.15.10(oxc-resolver@11.16.2)(rolldown@1.1.0)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.15 @@ -18029,8 +16392,6 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - type@2.7.3: {} - typescript@5.4.2: {} typescript@5.6.1-rc: {} @@ -18041,9 +16402,6 @@ snapshots: ufo@1.6.2: {} - uglify-js@3.19.3: - optional: true - ultracite@7.0.8(effect@3.20.0)(typescript@5.9.3): dependencies: '@clack/prompts': 0.11.0 @@ -18204,8 +16562,6 @@ snapshots: uuid@11.1.0: {} - uuid@8.3.2: {} - validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 @@ -18513,7 +16869,7 @@ snapshots: vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@25.0.6)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.17 '@vitest/runner': 4.0.17 '@vitest/snapshot': 4.0.17 @@ -18549,45 +16905,6 @@ snapshots: - tsx - yaml - vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): - dependencies: - '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.17 - '@vitest/runner': 4.0.17 - '@vitest/snapshot': 4.0.17 - '@vitest/spy': 4.0.17 - '@vitest/utils': 4.0.17 - es-module-lexer: 1.7.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@types/node': 25.0.6 - happy-dom: 20.1.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - vscode-uri@3.1.0: {} walk-up-path@4.0.0: {} @@ -18629,8 +16946,6 @@ snapshots: word-wrap@1.2.5: {} - wordwrap@1.0.0: {} - wrap-ansi@5.1.0: dependencies: ansi-styles: 3.2.1 From 0247f6fb0e936473a4e650941b78e63f4a2c9806 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:55:49 -0500 Subject: [PATCH 3/3] remove public cli --- .changeset/move-proofkit-cli-home.md | 6 - .../simplify-cli-init-doctor-typegen.md | 12 - .github/workflows/release.yml | 23 - knip.config.ts | 1 - package.json | 3 +- packages/cli/CHANGELOG.md | 571 ---------- packages/cli/CLI_FLOW_AUDIT.md | 111 -- packages/cli/README.md | 19 - packages/cli/bin/proofkit.cjs | 61 -- packages/cli/index.d.ts | 19 - packages/cli/package.json | 96 -- packages/cli/scripts/build-binaries.mjs | 101 -- packages/cli/scripts/build-current-binary.mjs | 22 - packages/cli/scripts/write-cli-version.mjs | 23 - packages/cli/src/cli/fmdapi.ts | 57 - packages/cli/src/cli/ottofms.ts | 277 ----- packages/cli/src/cli/prompts.ts | 186 ---- packages/cli/src/cli/typegen/index.ts | 30 - packages/cli/src/cli/utils.ts | 49 - packages/cli/src/consts.ts | 55 - packages/cli/src/core/context.ts | 241 ----- packages/cli/src/core/doctor.ts | 320 ------ packages/cli/src/core/errors.ts | 81 -- packages/cli/src/core/executeInitPlan.ts | 575 ---------- packages/cli/src/core/planInit.ts | 266 ----- packages/cli/src/core/resolveInitRequest.ts | 801 -------------- packages/cli/src/core/types.ts | 168 --- packages/cli/src/helpers/intent.ts | 18 - packages/cli/src/helpers/ultracite.ts | 94 -- packages/cli/src/index.ts | 423 -------- .../cli/src/installers/install-fm-addon.ts | 404 ------- .../cli/src/installers/proofkit-webviewer.ts | 133 --- packages/cli/src/package-versions.ts | 5 - packages/cli/src/services/live.ts | 844 --------------- packages/cli/src/state.ts | 31 - packages/cli/src/utils/browserOpen.ts | 11 - packages/cli/src/utils/getProofKitVersion.ts | 38 - packages/cli/src/utils/http.ts | 123 --- packages/cli/src/utils/nonInteractive.ts | 35 - packages/cli/src/utils/packageManager.ts | 20 - packages/cli/src/utils/parseNameAndPath.ts | 46 - packages/cli/src/utils/parseSettings.ts | 162 --- packages/cli/src/utils/projectFiles.ts | 350 ------- packages/cli/src/utils/projectName.ts | 63 -- packages/cli/src/utils/prompts.ts | 185 ---- packages/cli/src/utils/removeTrailingSlash.ts | 6 - packages/cli/src/utils/renderTitle.ts | 18 - packages/cli/src/utils/sortPackageJson.ts | 86 -- packages/cli/src/utils/validateAppName.ts | 29 - packages/cli/src/utils/versioning.ts | 3 - .../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 | 155 --- .../extras/emailTemplates/generic.tsx | 135 --- .../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 | 155 --- .../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 | 3 - .../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 - .../cli/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 - .../cli/template/extras/src/env/with-auth.ts | 31 - .../cli/template/extras/src/env/with-clerk.ts | 20 - .../cli/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 | 107 -- .../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 - .../cli/template/extras/src/trpc/react.tsx | 76 -- .../cli/template/extras/src/trpc/server.ts | 30 - packages/cli/template/extras/src/utils/api.ts | 68 -- .../template/extras/start-database/mysql.sh | 54 - .../extras/start-database/postgres.sh | 55 - .../template/nextjs-shadcn/.claude/CLAUDE.md | 327 ------ .../nextjs-shadcn/.cursor/rules/ultracite.mdc | 333 ------ .../nextjs-shadcn/.vscode/settings.json | 35 - packages/cli/template/nextjs-shadcn/AGENTS.md | 1 - packages/cli/template/nextjs-shadcn/CLAUDE.md | 1 - packages/cli/template/nextjs-shadcn/README.md | 23 - .../cli/template/nextjs-shadcn/_gitignore | 38 - .../template/nextjs-shadcn/components.json | 21 - .../cli/template/nextjs-shadcn/next.config.ts | 8 - .../cli/template/nextjs-shadcn/package.json | 42 - .../template/nextjs-shadcn/postcss.config.mjs | 5 - .../cli/template/nextjs-shadcn/proofkit.json | 6 - .../template/nextjs-shadcn/public/favicon.ico | Bin 15086 -> 0 bytes .../nextjs-shadcn/public/proofkit.png | Bin 52140 -> 0 bytes .../nextjs-shadcn/src/app/(main)/layout.tsx | 6 - .../nextjs-shadcn/src/app/(main)/page.tsx | 93 -- .../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/app/proofkit-route.ts | 19 - .../nextjs-shadcn/src/components/AppLogo.tsx | 5 - .../components/AppShell/internal/AppShell.tsx | 23 - .../AppShell/internal/Header.module.css | 33 - .../components/AppShell/internal/Header.tsx | 29 - .../AppShell/internal/HeaderMobileMenu.tsx | 26 - .../AppShell/internal/HeaderNavLink.tsx | 34 - .../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 | 44 - .../components/AppShell/slot-header-right.tsx | 24 - .../src/components/mode-toggle.tsx | 39 - .../src/components/providers.tsx | 13 - .../src/components/theme-provider.tsx | 11 - .../src/components/ui/button.tsx | 62 -- .../src/components/ui/dropdown-menu.tsx | 265 ----- .../src/components/ui/sonner.tsx | 31 - .../cli/template/nextjs-shadcn/src/lib/env.ts | 12 - .../template/nextjs-shadcn/src/lib/utils.ts | 7 - .../cli/template/nextjs-shadcn/tsconfig.json | 28 - .../cli/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 | 83 -- .../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 | 61 -- .../pages/nextjs/table-infinite/page.tsx | 11 - .../pages/nextjs/table-infinite/query.ts | 45 - .../pages/nextjs/table-infinite/table.tsx | 108 -- .../cli/template/pages/nextjs/table/page.tsx | 17 - .../cli/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 - .../cli/template/vite-wv/.claude/launch.json | 18 - .../template/vite-wv/.vscode/settings.json | 11 - packages/cli/template/vite-wv/AGENTS.md | 1 - packages/cli/template/vite-wv/CLAUDE.md | 1 - packages/cli/template/vite-wv/_gitignore | 20 - packages/cli/template/vite-wv/components.json | 21 - packages/cli/template/vite-wv/index.html | 13 - packages/cli/template/vite-wv/package.json | 50 - .../vite-wv/proofkit-typegen.config.jsonc | 18 - packages/cli/template/vite-wv/proofkit.json | 9 - .../cli/template/vite-wv/scripts/filemaker.js | 193 ---- .../cli/template/vite-wv/scripts/upload.js | 27 - packages/cli/template/vite-wv/src/app.tsx | 109 -- packages/cli/template/vite-wv/src/index.css | 91 -- .../cli/template/vite-wv/src/lib/utils.ts | 5 - packages/cli/template/vite-wv/src/main.tsx | 37 - packages/cli/template/vite-wv/src/router.tsx | 107 -- .../vite-wv/src/routes/query-demo.tsx | 42 - packages/cli/template/vite-wv/tsconfig.json | 15 - packages/cli/template/vite-wv/vite.config.ts | 21 - packages/cli/tests/browser-apps.smoke.test.ts | 98 -- packages/cli/tests/cli.test.ts | 169 --- packages/cli/tests/default-command.test.ts | 169 --- packages/cli/tests/doctor.test.ts | 75 -- packages/cli/tests/effect-test-utils.ts | 15 - packages/cli/tests/executor.test.ts | 787 -------------- packages/cli/tests/init-fixtures.ts | 65 -- .../init-non-interactive-failures.test.ts | 195 ---- .../cli/tests/init-scaffold-contract.test.ts | 309 ------ packages/cli/tests/install-fm-addon.test.ts | 125 --- packages/cli/tests/integration.test.ts | 315 ------ .../tests/legacy-project-name-utils.test.ts | 19 - packages/cli/tests/live-git-init.test.ts | 95 -- packages/cli/tests/non-interactive.test.ts | 73 -- packages/cli/tests/ottofms.test.ts | 53 - packages/cli/tests/planner.test.ts | 281 ----- packages/cli/tests/project-name.test.ts | 44 - packages/cli/tests/prompts.test.ts | 35 - packages/cli/tests/render-failure.test.ts | 33 - packages/cli/tests/resolve-init.test.ts | 859 --------------- packages/cli/tests/setup.ts | 13 - packages/cli/tests/test-layer.ts | 618 ----------- packages/cli/tests/test-utils.ts | 86 -- packages/cli/tests/webviewer-apps.test.ts | 157 --- packages/cli/tsconfig.json | 13 - packages/cli/tsdown.config.ts | 13 - packages/cli/vitest.config.ts | 18 - packages/cli/vitest.smoke.config.ts | 21 - packages/create-proofkit/CHANGELOG.md | 49 - packages/create-proofkit/README.md | 7 - packages/create-proofkit/package.json | 35 - .../create-proofkit/src/getUserPkgManager.js | 22 - packages/create-proofkit/src/index.js | 49 - packages/create-proofkit/tests/index.test.js | 104 -- packages/create-proofkit/vitest.config.ts | 8 - pnpm-lock.yaml | 990 +----------------- 327 files changed, 5 insertions(+), 24935 deletions(-) delete mode 100644 .changeset/move-proofkit-cli-home.md delete mode 100644 .changeset/simplify-cli-init-doctor-typegen.md delete mode 100644 packages/cli/CHANGELOG.md delete mode 100644 packages/cli/CLI_FLOW_AUDIT.md delete mode 100644 packages/cli/README.md delete mode 100644 packages/cli/bin/proofkit.cjs delete mode 100644 packages/cli/index.d.ts delete mode 100644 packages/cli/package.json delete mode 100644 packages/cli/scripts/build-binaries.mjs delete mode 100644 packages/cli/scripts/build-current-binary.mjs delete mode 100644 packages/cli/scripts/write-cli-version.mjs delete mode 100644 packages/cli/src/cli/fmdapi.ts delete mode 100644 packages/cli/src/cli/ottofms.ts delete mode 100644 packages/cli/src/cli/prompts.ts delete mode 100644 packages/cli/src/cli/typegen/index.ts delete mode 100644 packages/cli/src/cli/utils.ts delete mode 100644 packages/cli/src/consts.ts delete mode 100644 packages/cli/src/core/context.ts delete mode 100644 packages/cli/src/core/doctor.ts delete mode 100644 packages/cli/src/core/errors.ts delete mode 100644 packages/cli/src/core/executeInitPlan.ts delete mode 100644 packages/cli/src/core/planInit.ts delete mode 100644 packages/cli/src/core/resolveInitRequest.ts delete mode 100644 packages/cli/src/core/types.ts delete mode 100644 packages/cli/src/helpers/intent.ts delete mode 100644 packages/cli/src/helpers/ultracite.ts delete mode 100644 packages/cli/src/index.ts delete mode 100644 packages/cli/src/installers/install-fm-addon.ts delete mode 100644 packages/cli/src/installers/proofkit-webviewer.ts delete mode 100644 packages/cli/src/package-versions.ts delete mode 100644 packages/cli/src/services/live.ts delete mode 100644 packages/cli/src/state.ts delete mode 100644 packages/cli/src/utils/browserOpen.ts delete mode 100644 packages/cli/src/utils/getProofKitVersion.ts delete mode 100644 packages/cli/src/utils/http.ts delete mode 100644 packages/cli/src/utils/nonInteractive.ts delete mode 100644 packages/cli/src/utils/packageManager.ts delete mode 100644 packages/cli/src/utils/parseNameAndPath.ts delete mode 100644 packages/cli/src/utils/parseSettings.ts delete mode 100644 packages/cli/src/utils/projectFiles.ts delete mode 100644 packages/cli/src/utils/projectName.ts delete mode 100644 packages/cli/src/utils/prompts.ts delete mode 100644 packages/cli/src/utils/removeTrailingSlash.ts delete mode 100644 packages/cli/src/utils/renderTitle.ts delete mode 100644 packages/cli/src/utils/sortPackageJson.ts delete mode 100644 packages/cli/src/utils/validateAppName.ts delete mode 100644 packages/cli/src/utils/versioning.ts delete mode 100644 packages/cli/template/extras/_cursor/conditional-rules/nextjs-framework.mdc delete mode 100644 packages/cli/template/extras/_cursor/conditional-rules/npm.mdc delete mode 100644 packages/cli/template/extras/_cursor/conditional-rules/pnpm.mdc delete mode 100644 packages/cli/template/extras/_cursor/conditional-rules/yarn.mdc delete mode 100644 packages/cli/template/extras/_cursor/rules/cursor-rules.mdc delete mode 100644 packages/cli/template/extras/_cursor/rules/filemaker-api.mdc delete mode 100644 packages/cli/template/extras/_cursor/rules/troubleshooting-patterns.mdc delete mode 100644 packages/cli/template/extras/_cursor/rules/ui-components.mdc delete mode 100644 packages/cli/template/extras/config/drizzle-config-mysql.ts delete mode 100644 packages/cli/template/extras/config/drizzle-config-postgres.ts delete mode 100644 packages/cli/template/extras/config/drizzle-config-sqlite.ts delete mode 100644 packages/cli/template/extras/config/fmschema.config.mjs delete mode 100644 packages/cli/template/extras/config/get-query-client.ts delete mode 100644 packages/cli/template/extras/config/postcss.config.cjs delete mode 100644 packages/cli/template/extras/config/query-provider-vite.tsx delete mode 100644 packages/cli/template/extras/config/query-provider.tsx delete mode 100644 packages/cli/template/extras/emailProviders/none/email.tsx delete mode 100644 packages/cli/template/extras/emailProviders/plunk/email.tsx delete mode 100644 packages/cli/template/extras/emailProviders/plunk/service.ts delete mode 100644 packages/cli/template/extras/emailProviders/resend/email.tsx delete mode 100644 packages/cli/template/extras/emailProviders/resend/service.ts delete mode 100644 packages/cli/template/extras/emailTemplates/auth-code.tsx delete mode 100644 packages/cli/template/extras/emailTemplates/generic.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/login/actions.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/login/login-form.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/login/page.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/login/schema.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/signup/actions.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/signup/page.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/signup/schema.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/components/auth/actions.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/components/auth/protect.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/components/auth/redirect.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/components/auth/use-user.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/components/auth/user-menu.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/emails/auth-code.tsx delete mode 100644 packages/cli/template/extras/fmaddon-auth/middleware.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/server/auth/utils/encryption.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/server/auth/utils/index.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/server/auth/utils/password.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/server/auth/utils/redirect.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/server/auth/utils/session.ts delete mode 100644 packages/cli/template/extras/fmaddon-auth/server/auth/utils/user.ts delete mode 100644 packages/cli/template/extras/prisma/schema/base-planetscale.prisma delete mode 100644 packages/cli/template/extras/prisma/schema/base.prisma delete mode 100644 packages/cli/template/extras/prisma/schema/with-auth-planetscale.prisma delete mode 100644 packages/cli/template/extras/prisma/schema/with-auth.prisma delete mode 100644 packages/cli/template/extras/src/app/_components/post-tw.tsx delete mode 100644 packages/cli/template/extras/src/app/_components/post.tsx delete mode 100644 packages/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts delete mode 100644 packages/cli/template/extras/src/app/api/trpc/[trpc]/route.ts delete mode 100644 packages/cli/template/extras/src/app/clerk-auth/layout.tsx delete mode 100644 packages/cli/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx delete mode 100644 packages/cli/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx delete mode 100644 packages/cli/template/extras/src/app/layout/base.tsx delete mode 100644 packages/cli/template/extras/src/app/layout/main-shell.tsx delete mode 100644 packages/cli/template/extras/src/app/layout/with-trpc-tw.tsx delete mode 100644 packages/cli/template/extras/src/app/layout/with-trpc.tsx delete mode 100644 packages/cli/template/extras/src/app/layout/with-tw.tsx delete mode 100644 packages/cli/template/extras/src/app/next-auth/layout.tsx delete mode 100644 packages/cli/template/extras/src/app/next-auth/signin/page.tsx delete mode 100644 packages/cli/template/extras/src/app/next-auth/signup/action.ts delete mode 100644 packages/cli/template/extras/src/app/next-auth/signup/page.tsx delete mode 100644 packages/cli/template/extras/src/app/next-auth/signup/validation.ts delete mode 100644 packages/cli/template/extras/src/app/page/base.tsx delete mode 100644 packages/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx delete mode 100644 packages/cli/template/extras/src/app/page/with-auth-trpc.tsx delete mode 100644 packages/cli/template/extras/src/app/page/with-trpc-tw.tsx delete mode 100644 packages/cli/template/extras/src/app/page/with-trpc.tsx delete mode 100644 packages/cli/template/extras/src/app/page/with-tw.tsx delete mode 100644 packages/cli/template/extras/src/components/clerk-auth/clerk-provider.tsx delete mode 100644 packages/cli/template/extras/src/components/clerk-auth/user-menu-mobile.tsx delete mode 100644 packages/cli/template/extras/src/components/clerk-auth/user-menu.tsx delete mode 100644 packages/cli/template/extras/src/components/next-auth/next-auth-provider.tsx delete mode 100644 packages/cli/template/extras/src/components/next-auth/user-menu-mobile.tsx delete mode 100644 packages/cli/template/extras/src/components/next-auth/user-menu.tsx delete mode 100644 packages/cli/template/extras/src/env/with-auth.ts delete mode 100644 packages/cli/template/extras/src/env/with-clerk.ts delete mode 100644 packages/cli/template/extras/src/index.module.css delete mode 100644 packages/cli/template/extras/src/middleware/clerk.ts delete mode 100644 packages/cli/template/extras/src/middleware/next-auth.ts delete mode 100644 packages/cli/template/extras/src/pages/_app/base.tsx delete mode 100644 packages/cli/template/extras/src/pages/_app/with-auth-trpc-tw.tsx delete mode 100644 packages/cli/template/extras/src/pages/_app/with-auth-trpc.tsx delete mode 100644 packages/cli/template/extras/src/pages/_app/with-auth-tw.tsx delete mode 100644 packages/cli/template/extras/src/pages/_app/with-auth.tsx delete mode 100644 packages/cli/template/extras/src/pages/_app/with-trpc-tw.tsx delete mode 100644 packages/cli/template/extras/src/pages/_app/with-trpc.tsx delete mode 100644 packages/cli/template/extras/src/pages/_app/with-tw.tsx delete mode 100644 packages/cli/template/extras/src/pages/api/auth/[...nextauth].ts delete mode 100644 packages/cli/template/extras/src/pages/api/trpc/[trpc].ts delete mode 100644 packages/cli/template/extras/src/pages/index/base.tsx delete mode 100644 packages/cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx delete mode 100644 packages/cli/template/extras/src/pages/index/with-auth-trpc.tsx delete mode 100644 packages/cli/template/extras/src/pages/index/with-trpc-tw.tsx delete mode 100644 packages/cli/template/extras/src/pages/index/with-trpc.tsx delete mode 100644 packages/cli/template/extras/src/pages/index/with-tw.tsx delete mode 100644 packages/cli/template/extras/src/server/api/root.ts delete mode 100644 packages/cli/template/extras/src/server/api/routers/post/base.ts delete mode 100644 packages/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts delete mode 100644 packages/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts delete mode 100644 packages/cli/template/extras/src/server/api/routers/post/with-auth.ts delete mode 100644 packages/cli/template/extras/src/server/api/routers/post/with-drizzle.ts delete mode 100644 packages/cli/template/extras/src/server/api/routers/post/with-prisma.ts delete mode 100644 packages/cli/template/extras/src/server/api/trpc-app/base.ts delete mode 100644 packages/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts delete mode 100644 packages/cli/template/extras/src/server/api/trpc-app/with-auth.ts delete mode 100644 packages/cli/template/extras/src/server/api/trpc-app/with-db.ts delete mode 100644 packages/cli/template/extras/src/server/api/trpc-pages/base.ts delete mode 100644 packages/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts delete mode 100644 packages/cli/template/extras/src/server/api/trpc-pages/with-auth.ts delete mode 100644 packages/cli/template/extras/src/server/api/trpc-pages/with-db.ts delete mode 100644 packages/cli/template/extras/src/server/data/users.ts delete mode 100644 packages/cli/template/extras/src/server/db/db-prisma-planetscale.ts delete mode 100644 packages/cli/template/extras/src/server/db/db-prisma.ts delete mode 100644 packages/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts delete mode 100644 packages/cli/template/extras/src/server/db/index-drizzle/with-planetscale.ts delete mode 100644 packages/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts delete mode 100644 packages/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts delete mode 100644 packages/cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts delete mode 100644 packages/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts delete mode 100644 packages/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts delete mode 100644 packages/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts delete mode 100644 packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts delete mode 100644 packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts delete mode 100644 packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts delete mode 100644 packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts delete mode 100644 packages/cli/template/extras/src/server/next-auth/base.ts delete mode 100644 packages/cli/template/extras/src/server/next-auth/password.ts delete mode 100644 packages/cli/template/extras/src/server/next-auth/with-drizzle.ts delete mode 100644 packages/cli/template/extras/src/server/next-auth/with-prisma.ts delete mode 100644 packages/cli/template/extras/src/trpc/query-client.ts delete mode 100644 packages/cli/template/extras/src/trpc/react.tsx delete mode 100644 packages/cli/template/extras/src/trpc/server.ts delete mode 100644 packages/cli/template/extras/src/utils/api.ts delete mode 100755 packages/cli/template/extras/start-database/mysql.sh delete mode 100755 packages/cli/template/extras/start-database/postgres.sh delete mode 100644 packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md delete mode 100644 packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc delete mode 100644 packages/cli/template/nextjs-shadcn/.vscode/settings.json delete mode 100644 packages/cli/template/nextjs-shadcn/AGENTS.md delete mode 100644 packages/cli/template/nextjs-shadcn/CLAUDE.md delete mode 100644 packages/cli/template/nextjs-shadcn/README.md delete mode 100644 packages/cli/template/nextjs-shadcn/_gitignore delete mode 100644 packages/cli/template/nextjs-shadcn/components.json delete mode 100644 packages/cli/template/nextjs-shadcn/next.config.ts delete mode 100644 packages/cli/template/nextjs-shadcn/package.json delete mode 100644 packages/cli/template/nextjs-shadcn/postcss.config.mjs delete mode 100644 packages/cli/template/nextjs-shadcn/proofkit.json delete mode 100644 packages/cli/template/nextjs-shadcn/public/favicon.ico delete mode 100644 packages/cli/template/nextjs-shadcn/public/proofkit.png delete mode 100644 packages/cli/template/nextjs-shadcn/src/app/(main)/layout.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/app/globals.css delete mode 100644 packages/cli/template/nextjs-shadcn/src/app/layout.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/app/navigation.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/app/proofkit-route.ts delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppLogo.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/config.ts delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/mode-toggle.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/providers.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/theme-provider.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/ui/button.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/components/ui/sonner.tsx delete mode 100644 packages/cli/template/nextjs-shadcn/src/lib/env.ts delete mode 100644 packages/cli/template/nextjs-shadcn/src/lib/utils.ts delete mode 100644 packages/cli/template/nextjs-shadcn/tsconfig.json delete mode 100644 packages/cli/template/pages/nextjs/blank/page.tsx delete mode 100644 packages/cli/template/pages/nextjs/table-edit/actions.ts delete mode 100644 packages/cli/template/pages/nextjs/table-edit/page.tsx delete mode 100644 packages/cli/template/pages/nextjs/table-edit/schema.ts delete mode 100644 packages/cli/template/pages/nextjs/table-edit/table.tsx delete mode 100644 packages/cli/template/pages/nextjs/table-infinite-edit/actions.ts delete mode 100644 packages/cli/template/pages/nextjs/table-infinite-edit/page.tsx delete mode 100644 packages/cli/template/pages/nextjs/table-infinite-edit/query.ts delete mode 100644 packages/cli/template/pages/nextjs/table-infinite-edit/schema.ts delete mode 100644 packages/cli/template/pages/nextjs/table-infinite-edit/table.tsx delete mode 100644 packages/cli/template/pages/nextjs/table-infinite/actions.ts delete mode 100644 packages/cli/template/pages/nextjs/table-infinite/page.tsx delete mode 100644 packages/cli/template/pages/nextjs/table-infinite/query.ts delete mode 100644 packages/cli/template/pages/nextjs/table-infinite/table.tsx delete mode 100644 packages/cli/template/pages/nextjs/table/page.tsx delete mode 100644 packages/cli/template/pages/nextjs/table/table.tsx delete mode 100644 packages/cli/template/pages/vite-wv/blank/index.tsx delete mode 100644 packages/cli/template/pages/vite-wv/table-edit/index.tsx delete mode 100644 packages/cli/template/pages/vite-wv/table/index.tsx delete mode 100644 packages/cli/template/vite-wv/.claude/launch.json delete mode 100644 packages/cli/template/vite-wv/.vscode/settings.json delete mode 100644 packages/cli/template/vite-wv/AGENTS.md delete mode 100644 packages/cli/template/vite-wv/CLAUDE.md delete mode 100644 packages/cli/template/vite-wv/_gitignore delete mode 100644 packages/cli/template/vite-wv/components.json delete mode 100644 packages/cli/template/vite-wv/index.html delete mode 100644 packages/cli/template/vite-wv/package.json delete mode 100644 packages/cli/template/vite-wv/proofkit-typegen.config.jsonc delete mode 100644 packages/cli/template/vite-wv/proofkit.json delete mode 100644 packages/cli/template/vite-wv/scripts/filemaker.js delete mode 100644 packages/cli/template/vite-wv/scripts/upload.js delete mode 100644 packages/cli/template/vite-wv/src/app.tsx delete mode 100644 packages/cli/template/vite-wv/src/index.css delete mode 100644 packages/cli/template/vite-wv/src/lib/utils.ts delete mode 100644 packages/cli/template/vite-wv/src/main.tsx delete mode 100644 packages/cli/template/vite-wv/src/router.tsx delete mode 100644 packages/cli/template/vite-wv/src/routes/query-demo.tsx delete mode 100644 packages/cli/template/vite-wv/tsconfig.json delete mode 100644 packages/cli/template/vite-wv/vite.config.ts delete mode 100644 packages/cli/tests/browser-apps.smoke.test.ts delete mode 100644 packages/cli/tests/cli.test.ts delete mode 100644 packages/cli/tests/default-command.test.ts delete mode 100644 packages/cli/tests/doctor.test.ts delete mode 100644 packages/cli/tests/effect-test-utils.ts delete mode 100644 packages/cli/tests/executor.test.ts delete mode 100644 packages/cli/tests/init-fixtures.ts delete mode 100644 packages/cli/tests/init-non-interactive-failures.test.ts delete mode 100644 packages/cli/tests/init-scaffold-contract.test.ts delete mode 100644 packages/cli/tests/install-fm-addon.test.ts delete mode 100644 packages/cli/tests/integration.test.ts delete mode 100644 packages/cli/tests/legacy-project-name-utils.test.ts delete mode 100644 packages/cli/tests/live-git-init.test.ts delete mode 100644 packages/cli/tests/non-interactive.test.ts delete mode 100644 packages/cli/tests/ottofms.test.ts delete mode 100644 packages/cli/tests/planner.test.ts delete mode 100644 packages/cli/tests/project-name.test.ts delete mode 100644 packages/cli/tests/prompts.test.ts delete mode 100644 packages/cli/tests/render-failure.test.ts delete mode 100644 packages/cli/tests/resolve-init.test.ts delete mode 100644 packages/cli/tests/setup.ts delete mode 100644 packages/cli/tests/test-layer.ts delete mode 100644 packages/cli/tests/test-utils.ts delete mode 100644 packages/cli/tests/webviewer-apps.test.ts delete mode 100644 packages/cli/tsconfig.json delete mode 100644 packages/cli/tsdown.config.ts delete mode 100644 packages/cli/vitest.config.ts delete mode 100644 packages/cli/vitest.smoke.config.ts delete mode 100644 packages/create-proofkit/CHANGELOG.md delete mode 100644 packages/create-proofkit/README.md delete mode 100644 packages/create-proofkit/package.json delete mode 100644 packages/create-proofkit/src/getUserPkgManager.js delete mode 100644 packages/create-proofkit/src/index.js delete mode 100644 packages/create-proofkit/tests/index.test.js delete mode 100644 packages/create-proofkit/vitest.config.ts diff --git a/.changeset/move-proofkit-cli-home.md b/.changeset/move-proofkit-cli-home.md deleted file mode 100644 index 0f52902c..00000000 --- a/.changeset/move-proofkit-cli-home.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@proofkit/cli": patch -"create-proofkit": patch ---- - -Move current ProofKit CLI and create-proofkit packages back into public release flow. diff --git a/.changeset/simplify-cli-init-doctor-typegen.md b/.changeset/simplify-cli-init-doctor-typegen.md deleted file mode 100644 index 3e43f632..00000000 --- a/.changeset/simplify-cli-init-doctor-typegen.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@proofkit/cli": minor -"@proofkit/typegen": minor ---- - -Simplify the ProofKit CLI to `init`, `doctor`, and `typegen`. - -- Remove the `add`, `remove`, `deploy`, `upgrade`, and `prompt` subcommands and their supporting installers/generators/helpers. The Web Viewer add-on is now downloaded and opened during `proofkit init`. -- `proofkit typegen` is now a thin alias that delegates entirely to `@proofkit/typegen` with no duplicated generation logic. Supports `--config`, `--env-path`, `--proofkit-token`, and `--reset-overrides`. -- Drop the Commander dependency; the CLI is now built entirely on `@effect/cli`. - -`@proofkit/typegen` now exposes its CLI runner through a new `@proofkit/typegen/cli` entrypoint (`runCli`). diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f91c46ab..6a09c70d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,29 +110,6 @@ jobs: - name: Build run: pnpm ci:build - cli-smoke: - name: CLI External Integration Smoke Tests - if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v6 - - - name: Enable Corepack - run: corepack enable - - - name: Setup Node.js 22.x - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: "pnpm" - - - name: Install Dependencies - run: pnpm install --frozen-lockfile - - - name: Run CLI External Integration Smoke Tests - run: pnpm ci:cli-smoke - fmodata-e2e: name: fmodata E2E Tests if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' diff --git a/knip.config.ts b/knip.config.ts index b316b614..a073e39d 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -1,7 +1,6 @@ import type { KnipConfig } from "knip"; const config: KnipConfig = { - ignore: ["packages/cli/template/**"], ignoreBinaries: ["op", "vercel"], }; diff --git a/package.json b/package.json index 15223cd2..2eee691b 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,10 @@ "build": "turbo run build --filter={./packages/*} --filter=@proofkit/docs", "ci": "pnpm ci:pr", "ci:build": "pnpm build", - "ci:cli-smoke": "pnpm --filter @proofkit/cli build && pnpm exec varlock run -- pnpm --filter @proofkit/cli test:smoke", "ci:fmodata-e2e": "pnpm --filter @proofkit/fmodata test:e2e", "ci:lint": "pnpm lint && pnpm skill:check-versions", "ci:pr": "pnpm ci:lint && pnpm ci:typecheck && pnpm ci:test && pnpm ci:build", - "ci:release": "pnpm ci:lint && pnpm ci:typecheck && pnpm ci:test && pnpm ci:cli-smoke && pnpm ci:fmodata-e2e", + "ci:release": "pnpm ci:lint && pnpm ci:typecheck && pnpm ci:test && pnpm ci:fmodata-e2e", "ci:test": "pnpm test", "ci:typecheck": "pnpm typecheck", "dev": "turbo run dev", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md deleted file mode 100644 index 4f2c81a7..00000000 --- a/packages/cli/CHANGELOG.md +++ /dev/null @@ -1,571 +0,0 @@ -# @proofgeist/kit - -## 2.2.2 - -### Patch Changes - -- ff18231: Read ProofKit CLI version from package.json at build time. - -## 2.2.2-beta.0 - -### Patch Changes - -- ff18231: Read ProofKit CLI version from package.json at build time. - -## 2.2.1 - -### Patch Changes - -- Fix CLI version reporting in installer binary. The bundled proofkit binary now regenerates package-versions.ts before compilation, ensuring the correct version is baked in. - -## 2.2.0 - -### Minor Changes - -- 7d48139: Ship ProofKit CLI in installer and publish CLI packages. - -### Patch Changes - -- b1d4a68: Publish CLI beta releases to npm when Changesets is in beta pre mode. -- cd0f06f: Set installed proofkit launcher template root from app payload and remove redundant agent setup next step. -- 34b3c3c: Fix lint failures in freshly scaffolded projects: - - Stop writing a competing legacy `.oxlintrc.json` alongside Ultracite v7's scaffolded `oxlint.config.ts` for webviewer apps. The CLI now writes `oxlint.config.ts` for both browser and webviewer app types. - - Drop the `// @ts-nocheck` directive from the generated `oxlint.config.ts`. It is no longer needed (oxlint v1 ships proper types for `defineConfig`) and was itself failing Ultracite's `typescript/ban-ts-comment` rule. - - Remove unused `redirect` import from the `vite-wv` `router.tsx` template that was triggering an `eslint/no-unused-vars` error on first lint. - -- d58e177: Include initial props wiring in generated WebViewer apps. -- dbab7b3: Fix ultracite init during scaffold; now uses latest major version 7 -- 50dacdf: Add transient ProofKit token passthrough for local FileMaker MCP setup. -- a0604e8: Remove generated app dependency on `@proofkit/cli`, allow compatible package manager minor versions, and print recovery details when dependency install fails during init. -- deb859d: Install Bun before npm publish binary build. -- 2a11681: Raise generated app Node floor to avoid pnpm skipping native oxlint bindings. - -## 2.2.0-beta.6 - -### Patch Changes - -- 34b3c3c: Fix lint failures in freshly scaffolded projects: - - Stop writing a competing legacy `.oxlintrc.json` alongside Ultracite v7's scaffolded `oxlint.config.ts` for webviewer apps. The CLI now writes `oxlint.config.ts` for both browser and webviewer app types. - - Drop the `// @ts-nocheck` directive from the generated `oxlint.config.ts`. It is no longer needed (oxlint v1 ships proper types for `defineConfig`) and was itself failing Ultracite's `typescript/ban-ts-comment` rule. - - Remove unused `redirect` import from the `vite-wv` `router.tsx` template that was triggering an `eslint/no-unused-vars` error on first lint. - -## 2.2.0-beta.5 - -### Patch Changes - -- deb859d: Install Bun before npm publish binary build. -- 2a11681: Raise generated app Node floor to avoid pnpm skipping native oxlint bindings. - -## 2.2.0-beta.4 - -### Patch Changes - -- dbab7b3: Fix ultracite init during scaffold; now uses latest major version 7 - -## 2.2.0-beta.3 - -### Patch Changes - -- b1d4a68: Publish CLI beta releases to npm when Changesets is in beta pre mode. -- d58e177: Include initial props wiring in generated WebViewer apps. -- a0604e8: Remove generated app dependency on `@proofkit/cli`, allow compatible package manager minor versions, and print recovery details when dependency install fails during init. - -## 2.1.0-beta.1 - -### Patch Changes - -- cd0f06f: Set installed proofkit launcher template root from app payload and remove redundant agent setup next step. - -## 2.1.0-beta.0 - -### Minor Changes - -- 7d48139: Ship ProofKit CLI in installer and publish CLI packages. - -### Patch Changes - -- 50dacdf: Add transient ProofKit token passthrough for local FileMaker MCP setup. - -## 2.0.5 - -### Patch Changes - -- b80e426: Fix Next template GitHub icon import. -- b62a73c: Restrict Node engines to 22, 24, or 26. -- 5cd1375: Write npm min-release-age config during npm project scaffolding. -- b62a73c: Parse generated package-manager commands with shell-style quoting. -- ea479ed: Add oxlint to generated app dev dependencies. -- 5cd1375: Run Ultracite and TanStack Intent setup during project scaffolding. - -## 2.0.4 - -### Patch Changes - -- 64b43aa: Use package manager execute command in init next steps. -- f1dd2c5: Prefer pnpm when npm invokes scaffolding and warn npm fallback users to use pnpm 11+. -- e13c759: Use devEngines packageManager in generated apps. -- f1dd2c5: Use package-manager exec command in generated typegen scripts. - -## 2.0.3 - -### Patch Changes - -- 16a0542: Publish prebuilt CLI binaries and cut install-time runtime deps so `npx` and `pnpm dlx` flows avoid dependency build approvals. - -## 2.0.2 - -### Patch Changes - -- cbf7bc7: Fix pnpm 11 scaffold installs in release smoke tests, including browser app build-script policy generation. -- d2947e1: Add pnpm 11 build policy to generated WebViewer projects so fresh installs can complete without broad dependency script approval. - -## 2.0.1 - -### Patch Changes - -- b3f820a: Use caret versions for scaffolded ProofKit deps. -- f1869dd: Download FileMaker add-ons from CDN instead of bundled CLI templates. - -## 2.0.0 - -### Major Changes - -- d3c7979: ProofKit CLI v2 - - Rewrite the CLI internals on Effect for tagged errors, observability, composability, and cleaner top-level error output. User cancellations are unified and rendered consistently. - - Refocus the CLI around project bootstrap and diagnostics: new `doctor` and placeholder `prompt` commands, updated default guidance and docs, and scaffolded typegen scripts now use the package-native `@proofkit/typegen` commands. - - Default new projects to shadcn/ui. The legacy Mantine scaffold (`nextjs-mantine`) and the `--ui` init flag have been removed; existing Mantine projects are not actively supported. - - Limit `proofkit add` to supported ProofKit add-ons (the unused registry-backed install path is removed). To add new pages or auth, pass the component name (e.g. `proofkit add table/basic`). - -### Minor Changes - -- b73b0d7: Rebrand FM HTTP → FM MCP across the stack: adapter, config fields, and all references now use `fm-mcp` / `FmMcp` to reflect the FileMaker MCP server branding. Adds an optional `fmMcp` typegen config for using an FM MCP proxy during metadata fetching, revamps the Web Viewer Vite template, hardens `proofkit init` (ignores hidden files, improves non-interactive prompts, stops generating Cursor rules), installs typegen skills locally when scaffolding, and ships initial Codex skills for fmdapi/fmodata/webviewer. - -### Patch Changes - -- 41c07ba: Auto-detect non-interactive terminals for CLI commands in CI, scripted runs, and coding-agent environments. -- 03294e5: Init now writes `CLAUDE.md` as `@AGENTS.md` and adds `.cursorignore` to keep `CLAUDE.md` out of Cursor scans. -- bacdb7d: Make initial git commit failures non-blocking during CLI init. -- 8818805: Fix `proofkit add addon` so it works outside an existing ProofKit project. -- 63d309b: Fix browser FileMaker scaffolds to install `@proofkit/typegen` and run the local `typegen` bin during initial codegen. -- e6d0c55: Improve the local ProofKit MCP / FileMaker setup flow during webviewer init: - - Install the local addon files before prompting that no FileMaker file is open. - - After retry, report the connected FileMaker file and prompt to choose when multiple files are open. - - Require an explicit local FileMaker file selection in non-interactive multi-file setups, and persist the selection (or the lone connected file) into the generated `proofkit-typegen.config.jsonc` as `fmMcp.connectedFileName`. - - Normalize non-interactive FileMaker layout names against live layout casing so case-only drift doesn't break hosted init or release smoke tests. - - Clarify the wording of the local FileMaker / ProofKit plugin setup prompts. - -- d5ca0e5: Preserve typed cancellation errors in the default project menu and wrap add/remove menu failures with stable error messages. -- 9add5ca: Project name parsing for `proofkit init`: normalize whitespace to dashes, lowercase `.`-derived names from the current directory, clarify that `.` means the current directory, and preserve leading directory segments verbatim in `parseNameAndPath`. -- be34116: Make scaffolded Web Viewer upload scripts use `deploy_html` as the canonical FileMaker script, with bridge-first and FMP URL fallback deployment. -- d7f86a4: Update newly scaffolded apps to use Ultracite for linting and formatting by default, including the generated `lint` and `format` scripts and CLI formatting flow. -- e0ea042: Update bundled FileMaker addon to fix a bug in the SendCallback script. -- c71b0d4: Add `utils/fmdapi` helper to the ProofKit registry. -- Updated dependencies - - @proofkit/typegen@1.1.0 - - @proofkit/fmdapi@5.1.0 - -## 2.0.0-beta.34 - -### Patch Changes - -- bacdb7d: Make initial git commit failures non-blocking during CLI init. - -## 2.0.0-beta.33 - -### Patch Changes - -- ee4c951: Clarify local FileMaker setup prompts for the ProofKit plugin flow. -- d5ca0e5: Preserve typed cancellation errors in the default project menu and wrap add/remove menu failures with stable error messages. -- ee4c951: Restore the interactive project menu when running `proofkit` inside an existing ProofKit project. -- be34116: Make scaffolded Web Viewer upload scripts use `deploy_html` as the canonical FileMaker script, with bridge-first and FMP URL fallback deployment. - - @proofkit/typegen@1.1.0-beta.27 - -## 2.0.0-beta.32 - -### Patch Changes - -- 7c7f70a: swap docs domain to proofkit.proof.sh -- 18ade4d: Limit `proofkit add` to supported ProofKit add-ons and remove the unused registry-backed install path. -- Updated dependencies [7c7f70a] - - @proofkit/fmdapi@5.1.0-beta.5 - - @proofkit/typegen@1.1.0-beta.26 - -## 2.0.0-beta.31 - -### Patch Changes - -- Updated dependencies [c031d74] - - @proofkit/typegen@1.1.0-beta.25 - -## 2.0.0-beta.30 - -### Patch Changes - -- 63d309b: Fix browser FileMaker scaffolds to install `@proofkit/typegen` and run the local `typegen` bin during initial codegen. -- d8fba3f: Normalize non-interactive FileMaker layout names against live layout casing so hosted init and release smoke tests do not fail on case-only layout drift. -- Updated dependencies [2f0f8f3] - - @proofkit/typegen@1.1.0-beta.24 - -## 2.0.0-beta.29 - -### Patch Changes - -- c79f183: Fix FileMaker webviewer init flow to install local addon files before prompting that no FileMaker file is open in the local MCP server. - - @proofkit/typegen@1.1.0-beta.23 - -## 2.0.0-beta.28 - -### Patch Changes - -- 8818805: Fix `proofkit add addon` so it works outside an existing ProofKit project. -- e0ea042: updated addon to fix a bug in the SendCallback script -- Updated dependencies [0643ddd] -- Updated dependencies [e6889d0] - - @proofkit/typegen@1.1.0-beta.22 - - @proofkit/fmdapi@5.1.0-beta.4 - -## 2.0.0-beta.27 - -### Patch Changes - -- 5bc5504: Init(webviewer): if local FM MCP reports exactly 1 connected file, persist it to `proofkit-typegen.config.jsonc` as `fmMcp.connectedFileName` during scaffold. -- 03294e5: Init now writes `CLAUDE.md` as `@AGENTS.md` and adds `.cursorignore` to keep `CLAUDE.md` out of Cursor scans. -- 4f40bfe: Normalize and validate `.`-derived CLI project names from the current directory consistently, including whitespace-to-dash conversion and lowercasing -- db11fda: Normalize only the final path segment in `parseNameAndPath`, preserving leading directory segments verbatim while keeping scoped-name parsing and `.` handling intact -- fe43be6: Drop the unused `nextjs-mantine` scaffold from the current CLI and always scaffold browser apps from `nextjs-shadcn`. -- 9add5ca: Remove the `--ui` init flag. ProofKit now only scaffolds shadcn. -- 9add5ca: Allow spaces in project names by normalizing them to dashes -- 9add5ca: Clarify that `.` uses the current directory for `proofkit init` -- Updated dependencies [c85574f] -- Updated dependencies [6da0c9a] - - @proofkit/typegen@1.1.0-beta.21 - -## 2.0.0-beta.26 - -### Patch Changes - -- e3b25c3: Refocus the ProofKit CLI around project bootstrap and diagnostics by adding `doctor` and placeholder `prompt` commands, updating default guidance and docs, and switching scaffolded typegen scripts to package-native `@proofkit/typegen` commands. - -## 2.0.0-beta.25 - -### Patch Changes - -- 41c07ba: Auto-detect non-interactive terminals for CLI commands in CI, scripted runs, and coding-agent environments. -- 1096f3b: Improve `proofkit init` error handling by using tagged Effect-based CLI errors for expected failures, unifying user cancellation, and rendering cleaner top-level error output. -- e6d0c55: Improve local ProofKit MCP setup messaging during webviewer init by reporting the connected FileMaker file after retry and prompting to choose a file when multiple files are open. -- 46696e4: Require `proofkit init` to use an explicit local FileMaker file selection in non-interactive multi-file setups, and save the selected local file into the generated typegen config. -- Updated dependencies [7b46a23] -- Updated dependencies [88242c2] - - @proofkit/typegen@1.1.0-beta.20 - -## 2.0.0-beta.23 - -### Minor Changes - -- b73b0d7: - cli: Revamp the Web Viewer 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 `fmMcp` config for using an FM MCP proxy during metadata fetching. - - fmdapi/fmodata/webviewer: Add initial Codex skills for client and integration workflows. -- b73b0d7: Rebrand FM HTTP → FM MCP across the stack. The adapter, config fields, and all references now use `fm-mcp` / `FmMcp` naming to reflect the FileMaker MCP server branding. - -### Patch Changes - -- Updated dependencies [b73b0d7] -- Updated dependencies [b73b0d7] - - @proofkit/typegen@1.1.0-beta.19 - - @proofkit/fmdapi@5.1.0-beta.3 - -## 2.0.0-beta.1 - -### Major Changes - -- d3c7979: Rewrite the CLI package for better observability, composability, and error tracing. - -### Patch Changes - -- d7f86a4: Update newly scaffolded apps to use Ultracite for linting and formatting by default, including the generated `lint` and `format` scripts and CLI formatting flow. - - @proofkit/typegen@1.1.0-beta.18 - -## 2.0.0-beta.22 - -### Minor Changes - -- 5544f68: - cli: Revamp the Web Viewer 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 Web Viewer 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/CLI_FLOW_AUDIT.md b/packages/cli/CLI_FLOW_AUDIT.md deleted file mode 100644 index 82e4bc31..00000000 --- a/packages/cli/CLI_FLOW_AUDIT.md +++ /dev/null @@ -1,111 +0,0 @@ -# ProofKit CLI Flow Audit - -## Actual runtime flow - -```mermaid -flowchart TD - A[proofkit] --> B{explicit subcommand?} - - B -->|no| C{proofkit.json in cwd?} - C -->|yes| D[print project guidance
doctor / prompt / init] - C -->|no + interactive| E[run init] - C -->|no + non-interactive| F[fail: explicit command required] - - B -->|init| G[Effect init flow] - G --> G1[resolve request] - G1 --> G2[plan init] - G2 --> G3[execute init plan] - - B -->|doctor| H[doctor audit] - B -->|prompt| I[placeholder note only] - - B -->|add| J{arg `name` present?} - J -->|addon| J1[add addon target] - J -->|tanstack-query| J2[run tanstack-query installer] - J -->|any other name| J3[installFromRegistry(name)] - J -->|no| J4{proofkit.json readable?} - J4 -->|no| J5[preflight add] - J5 --> J6[registry-driven add flow] - J4 -->|yes + ui=shadcn| J6 - J4 -->|yes + ui!=shadcn| J7[legacy interactive add menu] - J7 -->|page| J8[runAddPageAction] - J7 -->|schema| J9[runAddSchemaAction] - J7 -->|data| J10[runAddDataSourceCommand] - J7 -->|react-email| J11[runAddReactEmailCommand] - J7 -->|auth| J12[runAddAuthAction] - - B -->|remove| K[legacy interactive remove menu] - K -->|page| K1[runRemovePageAction] - K -->|schema| K2[runRemoveSchemaAction] - K -->|data| K3[runRemoveDataSourceCommand] - - B -->|typegen| L[legacy alias -> runCodegenCommand] - B -->|deploy| M[legacy deploy flow] - B -->|upgrade| N[legacy upgrade flow] -``` - -## Stranded legacy branches - -These paths still exist in legacy Commander modules, but new root routing does not expose them as subcommands: - -```mermaid -flowchart TD - A[legacy add Command] --> A1[add auth] - A --> A2[add addon] - A --> A3[add page] - A --> A4[add layout/schema] - A --> A5[add data] - - B[legacy remove Command] --> B1[remove page] - B --> B2[remove layout/schema] - B --> B3[remove data] - - C[legacy typegen Command] - D[legacy upgrade Command] -``` - -The new root CLI only exposes flat `add [name] [target]` and `remove [name]`, so those nested branches are currently implementation-only. - -## Gaps / dead ends - -1. `add` positional names misroute. - `runAdd()` special-cases only `addon` and `tanstack-query`, then sends every other provided `name` to `installFromRegistry(name)`. That means `proofkit add auth`, `proofkit add page`, `proofkit add schema`, `proofkit add layout`, `proofkit add data`, and `proofkit add react-email` do not hit their legacy handlers. They go to registry install instead. - -2. `remove [name]` is dead input. - `runRemove()` ignores `_name` entirely and always opens the interactive picker. So `proofkit remove page` does not remove a page directly. In non-interactive mode, this path has no direct branch and is likely unusable. - -3. Legacy subcommands still defined, but unreachable from root parser. - The root Effect CLI exposes `add` and `remove` as flat commands only. The nested Commander subcommands still exist under legacy `makeAddCommand()` and `makeRemoveCommand()`, but root help and parsing never surface `add auth`, `add page`, `remove page`, etc as true subcommands. - -4. `prompt` is a deliberate stub. - `proofkit prompt` exits successfully, but only prints a "coming soon" note. It is a real command but still a product dead end. - -5. Docs and runtime surface diverge. - Docs describe ProofKit as mainly `init`, `doctor`, and `prompt`, with package-native CLIs for ongoing work. Runtime still advertises `add`, `remove`, `typegen`, `deploy`, and `upgrade`. - -6. Naming drift: `schema` vs `layout`. - Legacy schema add/remove commands are actually named `layout` with alias `schema`. Interactive menus say "Schema". If nested subcommands come back, this naming split will still be confusing. - -## Tight fix list - -1. Pick one surface: flat verbs or nested subcommands. -2. If flat: - Make `runAdd(name)` dispatch explicit names to real handlers before registry fallback. -3. If flat: - Make `runRemove(name)` honor `page|schema|data`. -4. If nested: - Rebuild `add` and `remove` as Effect subcommand trees instead of flat arg parsers. -5. Hide or remove legacy commands still meant to be package-native only. -6. Either implement `prompt` or mark it hidden until ready. - -## Source refs - -- Root command surface: [packages/cli/src/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/index.ts:206) -- Root subcommand list: [packages/cli/src/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/index.ts:402) -- `add` dispatch: [packages/cli/src/cli/add/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/add/index.ts:102) -- Stranded legacy `add` subcommands: [packages/cli/src/cli/add/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/add/index.ts:166) -- `remove` ignoring arg: [packages/cli/src/cli/remove/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/remove/index.ts:12) -- Stranded legacy `remove` subcommands: [packages/cli/src/cli/remove/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/remove/index.ts:47) -- `layout` alias naming: [packages/cli/src/cli/add/fmschema.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/add/fmschema.ts:193) -- `prompt` stub: [packages/cli/src/core/prompt.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/core/prompt.ts:5) -- Docs surface: [apps/docs/content/docs/cli/reference/cli-commands.mdx](/Users/ericluce/Documents/Code/work/proofkit/apps/docs/content/docs/cli/reference/cli-commands.mdx:12) diff --git a/packages/cli/README.md b/packages/cli/README.md deleted file mode 100644 index f8e1efec..00000000 --- a/packages/cli/README.md +++ /dev/null @@ -1,19 +0,0 @@ -

- - Logo for ProofKit - -

- -

- 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.proof.sh](https://proofkit.proof.sh) diff --git a/packages/cli/bin/proofkit.cjs b/packages/cli/bin/proofkit.cjs deleted file mode 100644 index d83909a1..00000000 --- a/packages/cli/bin/proofkit.cjs +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const { existsSync } = require("node:fs"); -const path = require("node:path"); -const { spawnSync } = require("node:child_process"); - -const BINARIES = { - darwin: { - arm64: "proofkit-darwin-arm64", - x64: "proofkit-darwin-x64", - }, - linux: { - arm64: "proofkit-linux-arm64", - x64: "proofkit-linux-x64", - }, - win32: { - arm64: "proofkit-windows-arm64.exe", - x64: "proofkit-windows-x64.exe", - }, -}; - -function run(command, args) { - const result = spawnSync(command, args, { - stdio: "inherit", - env: { - ...process.env, - PROOFKIT_PKG_ROOT: path.resolve(__dirname, ".."), - }, - }); - - if (result.error) { - throw result.error; - } - - if (typeof result.status === "number") { - process.exit(result.status); - } - - process.exit(1); -} - -if (process.env.PROOFKIT_DISABLE_BUNDLED_BINARY !== "1") { - const binaryName = BINARIES[process.platform]?.[process.arch]; - if (binaryName) { - const binaryPath = path.join(__dirname, binaryName); - if (existsSync(binaryPath)) { - run(binaryPath, process.argv.slice(2)); - } - } -} - -const fallbackPath = path.join(__dirname, "..", "dist", "index.js"); -if (existsSync(fallbackPath)) { - run(process.execPath, [fallbackPath, ...process.argv.slice(2)]); -} - -console.error( - `No ProofKit executable found for ${process.platform}-${process.arch}.`, -); -process.exit(1); diff --git a/packages/cli/index.d.ts b/packages/cli/index.d.ts deleted file mode 100644 index 9c577672..00000000 --- a/packages/cli/index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/package.json b/packages/cli/package.json deleted file mode 100644 index 93bac81a..00000000 --- a/packages/cli/package.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "@proofkit/cli", - "version": "2.2.2", - "description": "Interactive CLI to scaffold and manage ProofKit projects", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/proofsh/proofkit.git", - "directory": "packages/cli" - }, - "keywords": [ - "proofkit", - "filemaker", - "ottomatic", - "proofgeist", - "proofsh", - "next.js", - "typescript" - ], - "type": "module", - "exports": { - ".": { - "types": "./index.d.ts", - "import": "./dist/index.js" - } - }, - "bin": { - "proofkit": "bin/proofkit.cjs" - }, - "files": [ - "bin/proofkit.cjs", - "bin/proofkit-*", - "bin/*.exe", - "dist/**/*.js", - "dist/**/*.d.ts", - "template", - "README.md", - "CHANGELOG.md", - "index.d.ts", - "package.json" - ], - "engines": { - "node": "^22.12.0 || ^24.0.0" - }, - "scripts": { - "typecheck": "node ./scripts/write-cli-version.mjs && tsc --noEmit", - "build": "node ./scripts/write-cli-version.mjs && NODE_ENV=production tsdown && publint --strict", - "build:binaries": "node ./scripts/write-cli-version.mjs && node ./scripts/build-binaries.mjs", - "prepublishOnly": "pnpm build && pnpm build:binaries", - "dev": "tsdown --watch", - "clean": "rm -rf dist .turbo node_modules bin/proofkit-* bin/*.exe", - "start": "node dist/index.js", - "lint": "cd ../.. && pnpm exec ultracite check packages/cli/src packages/cli/tests packages/cli/package.json packages/cli/tsconfig.json packages/cli/tsdown.config.ts packages/cli/vitest.config.ts", - "lint:summary": "pnpm run lint", - "release": "changeset version", - "pub:beta": "NODE_ENV=production pnpm build && pnpm --package npm@^11 dlx npm publish --tag beta --access public", - "pub:next": "NODE_ENV=production pnpm build && pnpm --package npm@^11 dlx npm publish --tag next --access public", - "pub:release": "NODE_ENV=production pnpm build && pnpm --package npm@^11 dlx npm publish --access public", - "test": "pnpm build && node ./scripts/build-current-binary.mjs && PROOFKIT_DISABLE_BUNDLED_BINARY=1 vitest run", - "test:smoke": "PROOFKIT_RUN_SMOKE_TESTS=1 vitest run --config vitest.smoke.config.ts" - }, - "devDependencies": { - "@clack/prompts": "^0.11.0", - "@effect/cli": "0.74.0", - "@effect/platform": "0.95.0", - "@effect/platform-node": "0.105.0", - "@effect/printer": "0.48.0", - "@effect/printer-ansi": "0.48.0", - "@inquirer/prompts": "^8.3.2", - "@proofkit/fmdapi": "workspace:*", - "@proofkit/typegen": "workspace:*", - "@types/fs-extra": "^11.0.4", - "@types/gradient-string": "^1.1.6", - "@types/node": "^22.19.5", - "@types/randomstring": "^1.3.0", - "axios": "^1.13.2", - "chalk": "5.4.1", - "dotenv": "^16.6.1", - "effect": "^3.20.0", - "execa": "^9.6.1", - "fs-extra": "^11.3.3", - "gradient-string": "^2.0.2", - "jsonc-parser": "^3.3.1", - "open": "^10.2.0", - "publint": "^0.3.16", - "randomstring": "^1.3.1", - "tsdown": "^0.14.2", - "type-fest": "^3.13.1", - "typescript": "^5.9.3", - "vitest": "^4.0.17", - "zod": "^4.3.5" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/cli/scripts/build-binaries.mjs b/packages/cli/scripts/build-binaries.mjs deleted file mode 100644 index ecce84cd..00000000 --- a/packages/cli/scripts/build-binaries.mjs +++ /dev/null @@ -1,101 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageRoot = path.resolve(__dirname, ".."); -const binDir = path.join(packageRoot, "bin"); -const entrypoint = path.join(packageRoot, "src", "index.ts"); - -const targets = [ - { target: "bun-darwin-arm64", file: "proofkit-darwin-arm64" }, - { target: "bun-darwin-x64", file: "proofkit-darwin-x64" }, - { target: "bun-linux-arm64", file: "proofkit-linux-arm64" }, - { target: "bun-linux-x64", file: "proofkit-linux-x64" }, - { target: "bun-windows-arm64", file: "proofkit-windows-arm64.exe" }, - { target: "bun-windows-x64", file: "proofkit-windows-x64.exe" }, -]; -const validTargets = new Set(targets.map((config) => config.target)); -const requestedTargetsEnv = process.env.PROOFKIT_BINARY_TARGETS ?? ""; - -const selectedTargets = new Set( - requestedTargetsEnv - .split(",") - .map((target) => target.trim()) - .filter(Boolean), -); -const filteredSelectedTargets = new Set( - [...selectedTargets].filter((target) => validTargets.has(target)), -); - -if (selectedTargets.size > 0 && filteredSelectedTargets.size === 0) { - console.error( - `No valid binary targets in PROOFKIT_BINARY_TARGETS="${requestedTargetsEnv}". Valid targets: ${targets - .map((config) => config.target) - .join(", ")}`, - ); - process.exit(1); -} - -mkdirSync(binDir, { recursive: true }); -for (const file of readdirSync(binDir)) { - if (file === "proofkit.cjs") { - continue; - } - rmSync(path.join(binDir, file), { recursive: true, force: true }); -} - -let builtCount = 0; -for (const config of targets) { - if ( - filteredSelectedTargets.size > 0 && - !filteredSelectedTargets.has(config.target) - ) { - continue; - } - - const outfile = path.join(binDir, config.file); - const result = spawnSync( - "bun", - [ - "build", - "--compile", - `--target=${config.target}`, - "--no-compile-autoload-dotenv", - "--no-compile-autoload-bunfig", - "--no-compile-autoload-tsconfig", - "--no-compile-autoload-package-json", - entrypoint, - `--outfile=${outfile}`, - ], - { - cwd: packageRoot, - stdio: "inherit", - env: process.env, - }, - ); - - if (result.error) { - throw result.error; - } - - if (result.status !== 0) { - process.exit(result.status ?? 1); - } - - if (existsSync(outfile) && !outfile.endsWith(".exe")) { - chmodSync(outfile, 0o755); - } - - builtCount += 1; -} - -if (builtCount === 0) { - console.error( - `No binary targets selected from PROOFKIT_BINARY_TARGETS="${requestedTargetsEnv}". Valid targets: ${targets - .map((config) => config.target) - .join(", ")}`, - ); - process.exit(1); -} diff --git a/packages/cli/scripts/build-current-binary.mjs b/packages/cli/scripts/build-current-binary.mjs deleted file mode 100644 index 2a90d4f1..00000000 --- a/packages/cli/scripts/build-current-binary.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { spawnSync } from "node:child_process"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageRoot = path.resolve(__dirname, ".."); -const target = `bun-${process.platform}-${process.arch}`; - -const result = spawnSync("node", ["./scripts/build-binaries.mjs"], { - cwd: packageRoot, - stdio: "inherit", - env: { - ...process.env, - PROOFKIT_BINARY_TARGETS: target, - }, -}); - -if (result.error) { - throw result.error; -} - -process.exit(result.status ?? 1); diff --git a/packages/cli/scripts/write-cli-version.mjs b/packages/cli/scripts/write-cli-version.mjs deleted file mode 100644 index 9e59ec03..00000000 --- a/packages/cli/scripts/write-cli-version.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageRoot = path.resolve(__dirname, ".."); - -const readVersion = (packagePath) => { - const packageJson = JSON.parse(readFileSync(packagePath, "utf8")); - return packageJson.version ?? "0.0.0-private"; -}; - -const outputPath = path.join(packageRoot, "src", "package-versions.ts"); -const content = [ - `export const CLI_VERSION: string = ${JSON.stringify(readVersion(path.join(packageRoot, "package.json")))};`, - `export const FMDAPI_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "fmdapi", "package.json")))} as const;`, - `export const BETTER_AUTH_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "better-auth", "package.json")))} as const;`, - `export const WEBVIEWER_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "webviewer", "package.json")))} as const;`, - `export const TYPEGEN_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "typegen", "package.json")))} as const;`, - "", -].join("\n"); - -writeFileSync(outputPath, content, "utf8"); diff --git a/packages/cli/src/cli/fmdapi.ts b/packages/cli/src/cli/fmdapi.ts deleted file mode 100644 index cb252b8a..00000000 --- a/packages/cli/src/cli/fmdapi.ts +++ /dev/null @@ -1,57 +0,0 @@ -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/src/cli/ottofms.ts b/packages/cli/src/cli/ottofms.ts deleted file mode 100644 index e6bb71a9..00000000 --- a/packages/cli/src/cli/ottofms.ts +++ /dev/null @@ -1,277 +0,0 @@ -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, reject) => { - let settled = false; - const pollingInterval = setInterval(() => { - axios - .get<{ response: WizardResponse }>(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { - "Accept-Encoding": "deflate", - }, - }) - .then((result) => { - if (settled) { - return; - } - settled = true; - 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(() => { - if (settled) { - return; - } - settled = true; - clearInterval(pollingInterval); - clearTimeout(timeout); - loginSpinner.stop("Login timed out. No worries - it happens to the best of us."); - reject(new Error("Login timed out")); - }, 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?", - }), - ); - 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/src/cli/prompts.ts b/packages/cli/src/cli/prompts.ts deleted file mode 100644 index ea4bc1b2..00000000 --- a/packages/cli/src/cli/prompts.ts +++ /dev/null @@ -1,186 +0,0 @@ -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; - 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 }) { - 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; - 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/src/cli/typegen/index.ts b/packages/cli/src/cli/typegen/index.ts deleted file mode 100644 index b1fab6b3..00000000 --- a/packages/cli/src/cli/typegen/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { runCli } from "@proofkit/typegen/cli"; - -export interface TypegenOptions { - config?: string; - envPath?: string; - proofkitToken?: string; - resetOverrides?: boolean; -} - -/** - * Thin alias to the `@proofkit/typegen` CLI. All config reading, validation, and - * generation lives in that package; we only map flags to its arg list so there is - * no duplicated logic to drift. - */ -export async function runTypegen(options: TypegenOptions = {}) { - const args: string[] = []; - if (options.config) { - args.push("--config", options.config); - } - if (options.envPath) { - args.push("--env-path", options.envPath); - } - if (options.proofkitToken) { - args.push("--proofkit-token", options.proofkitToken); - } - if (options.resetOverrides) { - args.push("--reset-overrides"); - } - await runCli(args); -} diff --git a/packages/cli/src/cli/utils.ts b/packages/cli/src/cli/utils.ts deleted file mode 100644 index d0d6a30d..00000000 --- a/packages/cli/src/cli/utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 { UserCancelledError } from "~/core/errors.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 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 UserCancelledError({ message: "User aborted the operation" }); - } - return value; -} diff --git a/packages/cli/src/consts.ts b/packages/cli/src/consts.ts deleted file mode 100644 index a76eb3c0..00000000 --- a/packages/cli/src/consts.ts +++ /dev/null @@ -1,55 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const distPath = path.dirname(__filename); -export const PKG_ROOT = process.env.PROOFKIT_PKG_ROOT ?? path.join(distPath, "../"); - -export const DEFAULT_APP_NAME = "my-proofkit-app"; -export const NODE_RUNTIME_VERSION = "^22.12.0 || ^24.0.0"; -export const cliName = "proofkit"; -export const npmName = "@proofkit/cli"; -export const DOCS_URL = "https://proofkit.proof.sh"; - -export function getAgentInstructions() { - return ` - ## ProofKit Documentation - ProofKit is a set of packages and opinions designed to work really well with FileMaker. Use the ProofKit docs as the primary reference for this project: https://proofkit.proof.sh/llms.txt - -## Data Loading -Always use tanstack/react-query instead of useState and useEffect when loading data or calling FileMaker scripts with fmFetch. - `; -} - -// Registry URL is injected at build time via tsdown define. -declare const __REGISTRY_URL__: string; -export const DEFAULT_REGISTRY_URL = - typeof __REGISTRY_URL__ !== "undefined" && __REGISTRY_URL__ ? __REGISTRY_URL__ : "https://proofkit.proof.sh"; -const TITLE_ASCII = ` - _______ ___ ___ ____ _ _ -|_ __ \\ .' ..]|_ ||_ _| (_) / |_ - | |__) |_ .--. .--. .--. _| |_ | |_/ / __ \`| |-' - | ___/[ \`/'\`\\]/ .'\`\\ \\/ .'\`\\ \\'-| |-' | __'. [ | | | - _| |_ | | | \\__. || \\__. | | | _| | \\ \\_ | | | |, -|_____| [___] '.__.' '.__.' [___] |____||____|[___]\\__/ -`; -export function getTitleText(version: string) { - const versionText = `v${version}`; - const lineWidth = 61; - const padding = Math.max(lineWidth - versionText.length, 0); - return `${TITLE_ASCII}${" ".repeat(padding)}${versionText}\n`; -} -function resolveTemplateRoot(): string { - const candidates = [path.join(PKG_ROOT, "template"), path.resolve(PKG_ROOT, "../cli/template")] as const; - - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - return candidate; - } - } - - return candidates[0]; -} - -export const TEMPLATE_ROOT = resolveTemplateRoot(); diff --git a/packages/cli/src/core/context.ts b/packages/cli/src/core/context.ts deleted file mode 100644 index 43572dd1..00000000 --- a/packages/cli/src/core/context.ts +++ /dev/null @@ -1,241 +0,0 @@ -import type { Effect as Fx } from "effect"; -import { Context } from "effect"; -import type { CliError } from "~/core/errors.js"; -import type { AppType, FileMakerEnvNames, FileMakerInputs, ProofKitSettings, UIType } from "~/core/types.js"; -import type { PackageManager } from "~/utils/packageManager.js"; - -type Eff = Fx.Effect; - -export interface CliContextValue { - cwd: string; - debug: boolean; - nonInteractive: boolean; - packageManager: PackageManager; - resolvedProjectConfig?: { - appType?: AppType; - ui?: UIType; - projectDir?: string; - }; -} - -export const CliContext = Context.GenericTag("@proofkit/cli/CliContext"); - -export interface PromptService { - readonly text: (options: { - message: string; - defaultValue?: string; - validate?: (value: string) => string | undefined; - }) => Promise; - readonly password: (options: { - message: string; - validate?: (value: string) => string | undefined; - }) => Promise; - readonly select: (options: { - message: string; - options: Array<{ - value: T; - label: string; - hint?: string; - disabled?: boolean | string; - }>; - }) => Promise; - readonly searchSelect: (options: { - message: string; - emptyMessage?: string; - options: Array<{ - value: T; - label: string; - hint?: string; - keywords?: string[]; - disabled?: boolean | string; - }>; - }) => Promise; - readonly multiSearchSelect: (options: { - message: string; - options: Array<{ - value: T; - label: string; - hint?: string; - keywords?: string[]; - disabled?: boolean | string; - }>; - required?: boolean; - }) => Promise; - readonly confirm: (options: { message: string; initialValue?: boolean }) => Promise; -} - -export const PromptService = Context.GenericTag("@proofkit/cli/PromptService"); - -export interface ConsoleService { - readonly info: (message: string) => void; - readonly warn: (message: string) => void; - readonly error: (message: string) => void; - readonly success: (message: string) => void; - readonly note: (message: string, title?: string) => void; -} - -export const ConsoleService = Context.GenericTag("@proofkit/cli/ConsoleService"); - -export interface FileSystemService { - readonly exists: (path: string) => Eff; - readonly readdir: (path: string) => Eff; - readonly ensureDir: (path: string) => Eff; - readonly emptyDir: (path: string) => Eff; - readonly copyDir: (from: string, to: string, options?: { overwrite?: boolean }) => Eff; - readonly rename: (from: string, to: string) => Eff; - readonly remove: (path: string) => Eff; - readonly readJson: (path: string) => Eff; - readonly writeJson: (path: string, value: unknown) => Eff; - readonly writeFile: (path: string, content: string) => Eff; - readonly readFile: (path: string) => Eff; -} - -export const FileSystemService = Context.GenericTag("@proofkit/cli/FileSystemService"); - -export interface TemplateService { - readonly getTemplateDir: (appType: AppType, ui: UIType) => string; -} - -export const TemplateService = Context.GenericTag("@proofkit/cli/TemplateService"); - -export interface PackageManagerService { - readonly getVersion: (packageManager: PackageManager, cwd: string) => Eff; -} - -export const PackageManagerService = Context.GenericTag("@proofkit/cli/PackageManagerService"); - -export interface ProcessService { - readonly run: ( - command: string, - args: string[], - options: { - cwd: string; - stdout?: "pipe" | "inherit" | "ignore"; - stderr?: "pipe" | "inherit" | "ignore"; - }, - ) => Eff<{ stdout: string; stderr: string }, CliError>; -} - -export const ProcessService = Context.GenericTag("@proofkit/cli/ProcessService"); - -export interface GitService { - readonly initialize: (projectDir: string) => Eff; -} - -export const GitService = Context.GenericTag("@proofkit/cli/GitService"); - -export interface SettingsService { - readonly writeSettings: (projectDir: string, settings: ProofKitSettings) => Eff; - readonly appendEnvVars: (projectDir: string, vars: Record) => Eff; -} - -export const SettingsService = Context.GenericTag("@proofkit/cli/SettingsService"); - -export interface FmMcpStatus { - baseUrl: string; - healthy: boolean; - connectedFiles: string[]; -} - -export interface FileMakerServerVersions { - fmsVersion: string; - ottoVersion: string | null; -} - -export interface OttoFileInfo { - filename: string; - status: string; -} - -export interface OttoApiKeyInfo { - key: string; - user: string; - database: string; - label: string; -} - -export interface FileMakerDataSourceEntry { - type: "fm"; - name: string; - envNames: FileMakerEnvNames; -} - -export interface FileMakerBootstrapArtifacts { - settings: ProofKitSettings; - envVars: Record; - envSchemaEntries: Array<{ - name: string; - zodSchema: string; - defaultValue: string; - }>; - typegenConfig: { - mode: FileMakerInputs["mode"]; - dataSourceName: string; - envNames?: FileMakerEnvNames; - fmMcpBaseUrl?: string; - connectedFileName?: string; - layoutName?: string; - schemaName?: string; - appType: AppType; - }; -} - -export interface FileMakerService { - readonly detectLocalFmMcp: (baseUrl?: string) => Eff; - readonly authorizeLocalFmMcp: (input: { - baseUrl: string; - fileName: string; - interactive: boolean; - clientName: string; - clientDescription: string; - }) => Eff<{ sessionToken: string }, CliError>; - readonly installLocalWebViewerAddon: () => Eff; - readonly validateHostedServerUrl: ( - serverUrl: string, - ottoPort?: number | null, - ) => Eff< - { - normalizedUrl: string; - versions: FileMakerServerVersions; - }, - CliError - >; - readonly getOttoFMSToken: (options: { url: URL }) => Eff<{ token: string }, CliError>; - readonly listFiles: (options: { url: URL; token: string }) => Eff; - readonly listAPIKeys: (options: { url: URL; token: string }) => Eff; - readonly createDataAPIKeyWithCredentials: (options: { - url: URL; - filename: string; - username: string; - password: string; - }) => Eff<{ apiKey: string }, CliError>; - readonly deployDemoFile: (options: { - url: URL; - token: string; - operation: "install" | "replace"; - }) => Eff<{ apiKey: string; filename: string }, CliError>; - readonly listLayouts: (options: { dataApiKey: string; fmFile: string; server: string }) => Eff; - readonly createFileMakerBootstrapArtifacts: ( - settings: ProofKitSettings, - inputs: FileMakerInputs, - appType: AppType, - ) => Eff; - readonly bootstrap: ( - projectDir: string, - settings: ProofKitSettings, - inputs: FileMakerInputs, - appType: AppType, - ) => Eff; -} - -export const FileMakerService = Context.GenericTag("@proofkit/cli/FileMakerService"); - -export interface CodegenService { - readonly runInitial: ( - projectDir: string, - packageManager: PackageManager, - proofkitToken?: string, - ) => Eff; -} - -export const CodegenService = Context.GenericTag("@proofkit/cli/CodegenService"); diff --git a/packages/cli/src/core/doctor.ts b/packages/cli/src/core/doctor.ts deleted file mode 100644 index 9ce75dff..00000000 --- a/packages/cli/src/core/doctor.ts +++ /dev/null @@ -1,320 +0,0 @@ -import path from "node:path"; -import { parse as parseDotenv } from "dotenv"; -import { Effect } from "effect"; -import { parse as parseJsonc } from "jsonc-parser/lib/esm/main.js"; -import { DOCS_URL } from "~/consts.js"; -import { CliContext, ConsoleService, FileSystemService } from "~/core/context.js"; - -interface TypegenConfigEntry { - type?: string; - path?: string; - fmMcp?: { enabled?: boolean; connectedFileName?: string }; - layouts?: unknown[]; - tables?: unknown[]; - envNames?: { - server?: string; - db?: string; - auth?: { - apiKey?: string; - username?: string; - password?: string; - }; - }; -} - -function pushUnique(target: string[], value: string | undefined) { - if (value && !target.includes(value)) { - target.push(value); - } -} - -function isTypegenConfigLike(value: unknown): value is { - config: TypegenConfigEntry | TypegenConfigEntry[]; -} { - return value !== null && typeof value === "object" && "config" in value; -} - -export const runDoctor = Effect.gen(function* () { - const cliContext = yield* CliContext; - const fs = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const cwd = cliContext.cwd; - const readJsonSafe = (targetPath: string) => - fs.readJson(targetPath).pipe( - Effect.match({ - onFailure: () => undefined, - onSuccess: (value) => value, - }), - ); - const readFileSafe = (targetPath: string) => - fs.readFile(targetPath).pipe( - Effect.match({ - onFailure: () => undefined, - onSuccess: (value) => value, - }), - ); - - const settingsPath = path.join(cwd, "proofkit.json"); - if (!(yield* fs.exists(settingsPath))) { - consoleService.note( - [ - "No ProofKit project found in this directory.", - "", - "Next steps:", - "- Run `proofkit init` to create a new project", - `- Docs: ${DOCS_URL}/docs/cli`, - ].join("\n"), - "Doctor", - ); - return; - } - - const findings: { level: "ok" | "warn" | "error"; message: string }[] = []; - - let settings: - | { - appType?: string; - envFile?: string; - dataSources?: { - type?: string; - envNames?: { - database?: string; - server?: string; - apiKey?: string; - }; - }[]; - } - | undefined; - - settings = yield* readJsonSafe(settingsPath); - if (settings) { - findings.push({ level: "ok", message: "Found `proofkit.json`." }); - } else { - findings.push({ - level: "error", - message: "Could not read `proofkit.json`.", - }); - } - - const packageJsonPath = path.join(cwd, "package.json"); - let packageJson: - | { - scripts?: Record; - dependencies?: Record; - devDependencies?: Record; - } - | undefined; - - if (yield* fs.exists(packageJsonPath)) { - const nextPackageJson = yield* readJsonSafe>(packageJsonPath); - if (nextPackageJson) { - packageJson = nextPackageJson; - const allDeps = { - ...(nextPackageJson.dependencies ?? {}), - ...(nextPackageJson.devDependencies ?? {}), - }; - - if (allDeps["@proofkit/typegen"]) { - findings.push({ level: "ok", message: "Found `@proofkit/typegen`." }); - } else { - findings.push({ - level: "warn", - message: "Missing `@proofkit/typegen` dependency.", - }); - } - - if (nextPackageJson.scripts?.typegen) { - findings.push({ level: "ok", message: "Found `typegen` script." }); - } else { - findings.push({ - level: "warn", - message: "Missing `typegen` script in `package.json`.", - }); - } - - if (nextPackageJson.scripts?.["typegen:ui"]) { - findings.push({ level: "ok", message: "Found `typegen:ui` script." }); - } - } else { - findings.push({ - level: "error", - message: "Could not read `package.json`.", - }); - } - } else { - findings.push({ level: "error", message: "Missing `package.json`." }); - } - - const typegenConfigPath = path.join(cwd, "proofkit-typegen.config.jsonc"); - let parsedTypegenConfig: - | { - config: TypegenConfigEntry | TypegenConfigEntry[]; - } - | undefined; - - if (yield* fs.exists(typegenConfigPath)) { - const raw = yield* readFileSafe(typegenConfigPath); - if (raw) { - const parsed = parseJsonc(raw); - if (isTypegenConfigLike(parsed)) { - parsedTypegenConfig = parsed; - findings.push({ - level: "ok", - message: "Typegen config is present and valid.", - }); - - const configEntries = Array.isArray(parsed.config) ? parsed.config : [parsed.config]; - for (const entry of configEntries) { - const outputPath = path.join(cwd, entry.path ?? "schema"); - if (yield* fs.exists(outputPath)) { - findings.push({ - level: "ok", - message: `Generated path exists: \`${entry.path ?? "schema"}\`.`, - }); - } else { - findings.push({ - level: "warn", - message: `Generated path missing: \`${entry.path ?? "schema"}\`. Run \`npx @proofkit/typegen\`.`, - }); - } - - if (entry.type === "fmdapi" && (entry.layouts?.length ?? 0) === 0) { - findings.push({ - level: "warn", - message: "Typegen config has no layouts yet. Use `npx @proofkit/typegen ui`.", - }); - } - - if (entry.type === "fmodata" && (entry.tables?.length ?? 0) === 0) { - findings.push({ - level: "warn", - message: "Typegen config has no tables yet. Use `npx @proofkit/typegen ui`.", - }); - } - - if (entry.type === "fmdapi" && entry.fmMcp?.enabled && !entry.fmMcp.connectedFileName) { - findings.push({ - level: "warn", - message: "FM MCP is enabled but no connected file is pinned yet.", - }); - } - } - } else { - findings.push({ - level: "error", - message: "Typegen config exists but is invalid. Open `npx @proofkit/typegen ui` or fix the JSONC file.", - }); - } - } else { - findings.push({ - level: "error", - message: "Could not read `proofkit-typegen.config.jsonc`.", - }); - } - } else { - findings.push({ - level: "warn", - message: "Missing `proofkit-typegen.config.jsonc`. Run `npx @proofkit/typegen init`.", - }); - } - - const envCandidates = [ - settings?.envFile ? path.join(cwd, settings.envFile) : undefined, - path.join(cwd, ".env.local"), - path.join(cwd, ".env"), - ].filter((value): value is string => Boolean(value)); - - let resolvedEnvPath: string | undefined; - for (const candidate of envCandidates) { - if (yield* fs.exists(candidate)) { - resolvedEnvPath = candidate; - break; - } - } - - const expectedEnvNames: string[] = []; - for (const source of settings?.dataSources ?? []) { - if (source.type !== "fm") { - continue; - } - pushUnique(expectedEnvNames, source.envNames?.server); - pushUnique(expectedEnvNames, source.envNames?.database); - pushUnique(expectedEnvNames, source.envNames?.apiKey); - } - - let configEntries: TypegenConfigEntry[] = []; - if (parsedTypegenConfig) { - configEntries = Array.isArray(parsedTypegenConfig.config) - ? parsedTypegenConfig.config - : [parsedTypegenConfig.config]; - } - - for (const entry of configEntries) { - pushUnique(expectedEnvNames, entry.envNames?.server); - pushUnique(expectedEnvNames, entry.envNames?.db); - pushUnique(expectedEnvNames, entry.envNames?.auth?.apiKey); - pushUnique(expectedEnvNames, entry.envNames?.auth?.username); - pushUnique(expectedEnvNames, entry.envNames?.auth?.password); - } - - if (expectedEnvNames.length > 0) { - if (resolvedEnvPath) { - const envRaw = yield* readFileSafe(resolvedEnvPath); - if (envRaw) { - const env = parseDotenv(envRaw); - const missing = expectedEnvNames.filter((name) => !(name in env)); - if (missing.length > 0) { - findings.push({ - level: "warn", - message: `Missing env vars in \`${path.basename(resolvedEnvPath)}\`: ${missing.join(", ")}.`, - }); - } else { - findings.push({ - level: "ok", - message: `Expected env vars found in \`${path.basename(resolvedEnvPath)}\`.`, - }); - } - } else { - findings.push({ - level: "error", - message: `Could not read env file \`${path.basename(resolvedEnvPath)}\`.`, - }); - } - } else { - findings.push({ - level: "warn", - message: `No env file found. Expected vars: ${expectedEnvNames.join(", ")}.`, - }); - } - } - - const errors = findings.filter((finding) => finding.level === "error"); - const warnings = findings.filter((finding) => finding.level === "warn"); - const oks = findings.filter((finding) => finding.level === "ok"); - - const lines = [ - `Checks: ${oks.length} ok, ${warnings.length} warn, ${errors.length} error`, - "", - ...findings.map((finding) => { - let prefix = "ERR"; - if (finding.level === "ok") { - prefix = "OK"; - } else if (finding.level === "warn") { - prefix = "WARN"; - } - return `- [${prefix}] ${finding.message}`; - }), - "", - "Next steps:", - "- Run `npx @proofkit/typegen init` if typegen config is missing", - "- Run `npx @proofkit/typegen ui` to edit typegen config", - "- Run `npx @proofkit/typegen` to regenerate generated files", - `- Docs: ${DOCS_URL}/docs/typegen`, - ]; - - if (settings?.appType === "webviewer") { - lines.splice(lines.length - 1, 0, "- For webviewer projects, make sure local FM MCP is running before typegen"); - } - - consoleService.note(lines.join("\n"), "Doctor"); -}); diff --git a/packages/cli/src/core/errors.ts b/packages/cli/src/core/errors.ts deleted file mode 100644 index d5aff9f0..00000000 --- a/packages/cli/src/core/errors.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Data } from "effect"; - -export class CliValidationError extends Data.TaggedError("CliValidationError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} - -export class UserCancelledError extends Data.TaggedError("UserCancelledError")<{ - readonly message: string; -}> {} - -export class NonInteractiveInputError extends Data.TaggedError("NonInteractiveInputError")<{ - readonly message: string; -}> {} - -export class DirectoryConflictError extends Data.TaggedError("DirectoryConflictError")<{ - readonly message: string; - readonly path: string; -}> {} - -export class FileMakerSetupError extends Data.TaggedError("FileMakerSetupError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} - -export class RegistryError extends Data.TaggedError("RegistryError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} - -export class ExternalCommandError extends Data.TaggedError("ExternalCommandError")<{ - readonly message: string; - readonly command: string; - readonly args: readonly string[]; - readonly cwd: string; - readonly cause?: unknown; -}> {} - -export class FileSystemError extends Data.TaggedError("FileSystemError")<{ - readonly message: string; - readonly operation: string; - readonly path: string; - readonly cause?: unknown; -}> {} - -export type CliError = - | CliValidationError - | UserCancelledError - | NonInteractiveInputError - | DirectoryConflictError - | FileMakerSetupError - | RegistryError - | ExternalCommandError - | FileSystemError; - -const cliErrorTags = new Set([ - "CliValidationError", - "UserCancelledError", - "NonInteractiveInputError", - "DirectoryConflictError", - "FileMakerSetupError", - "RegistryError", - "ExternalCommandError", - "FileSystemError", -]); - -export function isCliError(error: unknown): error is CliError { - return ( - typeof error === "object" && - error !== null && - "_tag" in error && - typeof error._tag === "string" && - "message" in error && - typeof error.message === "string" && - cliErrorTags.has(error._tag) - ); -} - -export function getCliErrorMessage(error: CliError) { - return error.message; -} diff --git a/packages/cli/src/core/executeInitPlan.ts b/packages/cli/src/core/executeInitPlan.ts deleted file mode 100644 index fee1390a..00000000 --- a/packages/cli/src/core/executeInitPlan.ts +++ /dev/null @@ -1,575 +0,0 @@ -import path from "node:path"; -import { Chalk } from "chalk"; -import { Cause, Effect, Exit } from "effect"; -import { getOrUndefined } from "effect/Option"; - -import { getAgentInstructions } from "~/consts.js"; -import { - CliContext, - CodegenService, - ConsoleService, - FileMakerService, - FileSystemService, - GitService, - PackageManagerService, - ProcessService, - PromptService, - SettingsService, -} from "~/core/context.js"; -import { - type CliError, - DirectoryConflictError, - type ExternalCommandError, - FileSystemError, - isCliError, - UserCancelledError, -} from "~/core/errors.js"; -import { applyPackageJsonMutations } from "~/core/planInit.js"; -import type { InitPlan } from "~/core/types.js"; -import { getIntentInstallCommand } from "~/helpers/intent.js"; -import { - getBrowserOxlintConfig, - getHuskyPreCommitHook, - getUltraciteInitCommand, - getWebViewerOxlintConfig, -} from "~/helpers/ultracite.js"; -import { - formatPackageManagerCommand, - normalizeImportAlias, - parseCommandString, - replaceTextInFiles, - updateTypegenConfig, -} from "~/utils/projectFiles.js"; -import { isCancel } from "~/utils/prompts.js"; -import { sortPackageJson } from "~/utils/sortPackageJson.js"; - -const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]); -const IMPORT_ALIAS_WILDCARD_REGEX = /\*/g; -const IMPORT_ALIAS_TRAILING_SLASH_REGEX = /\/?$/; -const chalk = new Chalk({ level: 1 }); - -const formatCommand = (command: string) => chalk.cyan(command); -const formatHeading = (heading: string) => chalk.bold(heading); -const formatPath = (value: string) => chalk.yellow(value); -const NPM_PACKAGE_MANAGER_WARNING = - "Warning: We strongly suggest using PNPM 11 or greater as your package manager to better protect your computer and your app."; - -function getCauseText(cause: unknown) { - if (typeof cause !== "object" || cause === null) { - return cause ? String(cause) : undefined; - } - - const details = cause as { - shortMessage?: unknown; - message?: unknown; - stderr?: unknown; - stdout?: unknown; - }; - - return [details.shortMessage, details.stderr, details.stdout, details.message] - .filter((value): value is string => typeof value === "string") - .map((value) => value.trim()) - .find((value) => value.length > 0); -} - -function formatExternalCommand(error: ExternalCommandError) { - return [error.command, ...error.args].join(" "); -} - -function isExternalCommandError(error: CliError): error is ExternalCommandError { - return error._tag === "ExternalCommandError"; -} - -function renderInstallFailure(plan: InitPlan, error: CliError) { - const failedCommand = isExternalCommandError(error) - ? formatExternalCommand(error) - : `${plan.request.packageManager} install`; - const lines = [ - chalk.red(formatHeading("Install failed.")), - `${formatHeading("Project root:")} ${formatPath(plan.targetDir)}`, - `${formatHeading("Failed command:")} ${formatCommand(failedCommand)}`, - `${formatHeading("Succeeded before failure:")} scaffold files, package.json, proofkit.json, env file, editor config files`, - ]; - const causeText = getCauseText("cause" in error ? error.cause : undefined); - if (causeText && causeText !== error.message) { - lines.push(`${formatHeading("Reason:")} ${causeText}`); - } else { - lines.push(`${formatHeading("Reason:")} ${error.message}`); - } - - lines.push( - "", - formatHeading("Continue troubleshooting:"), - ` ${formatCommand(`cd ${plan.request.appDir}`)}`, - ` ${formatCommand(failedCommand)}`, - "", - formatHeading("Start over:"), - ` Remove ${formatPath(plan.targetDir)}, then rerun the init command.`, - ); - - return lines.join("\n"); -} - -function renderNextSteps(plan: InitPlan, additionalSteps: string[] = []) { - const lines = [`${formatHeading("Project root:")} ${formatCommand(`cd ${formatPath(plan.request.appDir)}`)}`]; - - if (plan.request.noInstall) { - lines.push( - "", - formatHeading("Install dependencies:"), - ` ${formatCommand(plan.request.packageManager === "yarn" ? "yarn" : `${plan.request.packageManager} install`)}`, - ); - } - - if (plan.request.packageManager === "npm") { - lines.push("", chalk.yellow(NPM_PACKAGE_MANAGER_WARNING)); - } - - lines.push("", formatHeading("Start the app:"), ` ${formatCommand(`${plan.packageManagerCommand} dev`)}`); - - if (plan.request.appType === "webviewer") { - lines.push( - "", - formatHeading("When your FileMaker file is ready:"), - ` ${formatCommand(`${plan.packageManagerCommand} typegen`)}`, - ` ${formatCommand(`${plan.packageManagerCommand} launch-fm`)}`, - ); - - if (additionalSteps.length > 0) { - lines.push(...additionalSteps.map((step) => ` ${formatCommand(step)}`)); - } - } - - return lines.join("\n"); -} - -function getPackageScriptCommand(plan: InitPlan, scriptName: string) { - const [command, ...args] = parseCommandString(formatPackageManagerCommand(plan.request.packageManager, scriptName)); - if (!command) { - throw new Error(`Unable to resolve ${scriptName} command for ${plan.request.packageManager}.`); - } - return { command, args }; -} - -function getMeaningfulDirectoryEntries(entries: string[]) { - return entries.filter((entry) => { - if (AGENT_METADATA_DIRS.has(entry)) { - return false; - } - if (entry === ".gitignore") { - return true; - } - if (entry.startsWith(".")) { - return false; - } - return true; - }); -} - -function promptEffect(message: string, run: () => Promise, targetPath = "") { - return Effect.tryPromise({ - try: async () => { - const value = await run(); - if (isCancel(value)) { - throw new DirectoryConflictError({ - message, - path: targetPath, - }); - } - return value; - }, - catch: (cause) => - isCliError(cause) - ? cause - : new DirectoryConflictError({ - message, - path: targetPath, - }), - }); -} - -export const prepareDirectory = (plan: InitPlan) => - Effect.gen(function* () { - const fs = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const cliContext = yield* CliContext; - const prompts = yield* PromptService; - - const exists = yield* fs.exists(plan.targetDir); - if (!exists) { - return; - } - - const entries = yield* fs.readdir(plan.targetDir); - const meaningfulEntries = getMeaningfulDirectoryEntries(entries); - if (meaningfulEntries.length === 0) { - return; - } - - if (plan.request.force) { - yield* fs.emptyDir(plan.targetDir); - return; - } - - if (cliContext.nonInteractive) { - return yield* Effect.fail( - new DirectoryConflictError({ - message: `${plan.request.appDir} already exists and isn't empty. Remove the existing files or choose a different directory.`, - path: plan.targetDir, - }), - ); - } - - const overwriteMode = yield* promptEffect( - "Unable to choose how to handle the existing directory.", - () => - prompts.select({ - message: `${plan.request.appDir} already exists and isn't empty. How would you like to proceed?`, - options: [ - { value: "abort", label: "Abort installation" }, - { value: "clear", label: "Clear the directory and continue" }, - { - value: "overwrite", - label: "Continue and overwrite conflicting files", - }, - ], - }), - plan.targetDir, - ); - - if (overwriteMode === "abort") { - return yield* Effect.fail( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - } - - if (overwriteMode === "clear") { - const confirmed = yield* promptEffect( - "Unable to confirm directory clearing.", - () => - prompts.confirm({ - message: "Are you sure you want to clear the directory?", - initialValue: false, - }), - plan.targetDir, - ); - if (!confirmed) { - return yield* Effect.fail( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - } - yield* fs.emptyDir(plan.targetDir); - return; - } - - consoleService.warn(`Continuing in ${plan.request.appDir} and overwriting conflicting files when needed.`); - }); - -export const executeInitPlan = (plan: InitPlan) => - Effect.gen(function* () { - const cliContext = yield* CliContext; - const fs = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const settingsService = yield* SettingsService; - const fileMakerService = yield* FileMakerService; - const processService = yield* ProcessService; - const gitService = yield* GitService; - const codegenService = yield* CodegenService; - const packageManagerService = yield* PackageManagerService; - const additionalNextSteps: string[] = []; - const runFileSystemPromise = async (effect: Effect.Effect) => { - const exit = await Effect.runPromiseExit(effect); - if (Exit.isSuccess(exit)) { - return exit.value; - } - - const failure = getOrUndefined(Cause.failureOption(exit.cause)); - if (failure && typeof failure === "object" && failure !== null && "cause" in failure) { - throw failure.cause; - } - - throw failure ?? Cause.squash(exit.cause); - }; - const projectFilesFs = { - exists: (targetPath: string) => runFileSystemPromise(fs.exists(targetPath)), - readdir: (targetPath: string) => runFileSystemPromise(fs.readdir(targetPath)), - readFile: (targetPath: string) => runFileSystemPromise(fs.readFile(targetPath)), - writeFile: (targetPath: string, content: string) => runFileSystemPromise(fs.writeFile(targetPath, content)), - }; - - yield* prepareDirectory(plan); - - consoleService.info(`Scaffolding in ${plan.targetDir}`); - yield* fs.copyDir(plan.templateDir, plan.targetDir, { overwrite: true }); - - const stagedGitignore = path.join(plan.targetDir, "_gitignore"); - const finalGitignore = path.join(plan.targetDir, ".gitignore"); - if (yield* fs.exists(stagedGitignore)) { - if (yield* fs.exists(finalGitignore)) { - yield* fs.remove(stagedGitignore); - } else { - yield* fs.rename(stagedGitignore, finalGitignore); - } - } - - const packageJsonPath = path.join(plan.targetDir, "package.json"); - const packageJson = yield* fs.readJson>(packageJsonPath); - const updatedPackageJson = sortPackageJson( - applyPackageJsonMutations(packageJson as never, plan.packageJson) as never, - ); - yield* fs.writeJson(packageJsonPath, updatedPackageJson); - - yield* settingsService.writeSettings(plan.targetDir, plan.settings); - yield* fs.writeFile(plan.envFile.path, plan.envFile.content); - for (const write of plan.writes) { - yield* fs.writeFile(write.path, write.content); - } - - yield* Effect.tryPromise({ - try: () => replaceTextInFiles(projectFilesFs, plan.targetDir, "__PNPM_COMMAND__", plan.packageManagerCommand), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold placeholders.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - yield* Effect.tryPromise({ - try: () => - replaceTextInFiles( - projectFilesFs, - plan.targetDir, - "__PNPM_EXECUTE_COMMAND__", - plan.packageManagerExecuteCommand, - ), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold placeholders.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - yield* Effect.tryPromise({ - try: () => replaceTextInFiles(projectFilesFs, plan.targetDir, "__PACKAGE_MANAGER__", plan.request.packageManager), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold placeholders.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - yield* Effect.tryPromise({ - try: () => replaceTextInFiles(projectFilesFs, plan.targetDir, "__AGENT_INSTRUCTIONS__", getAgentInstructions()), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold placeholders.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - if (plan.request.importAlias !== "~/") { - yield* Effect.tryPromise({ - try: () => - replaceTextInFiles(projectFilesFs, plan.targetDir, "~/", normalizeImportAlias(plan.request.importAlias)), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold import aliases.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - yield* Effect.tryPromise({ - try: () => - replaceTextInFiles( - projectFilesFs, - plan.targetDir, - "@/", - plan.request.importAlias - .replace(IMPORT_ALIAS_WILDCARD_REGEX, "") - .replace(IMPORT_ALIAS_TRAILING_SLASH_REGEX, "/"), - ), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold import aliases.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - } - - let nextSettings = plan.settings; - if (plan.tasks.bootstrapFileMaker && plan.request.fileMaker) { - const fileMakerInputs = plan.request.fileMaker; - nextSettings = yield* fileMakerService.bootstrap( - plan.targetDir, - nextSettings, - fileMakerInputs, - plan.request.appType, - ); - yield* settingsService.writeSettings(plan.targetDir, nextSettings); - } - - if (plan.request.appType === "webviewer" && !plan.tasks.bootstrapFileMaker) { - const localFmMcp = yield* fileMakerService.detectLocalFmMcp(); - const connectedFiles = localFmMcp.connectedFiles.filter(Boolean); - if (localFmMcp.healthy && connectedFiles.length === 1) { - const detectedFile = connectedFiles[0]; - if (detectedFile) { - yield* Effect.tryPromise({ - try: () => - updateTypegenConfig(projectFilesFs, plan.targetDir, { - appType: "webviewer", - dataSourceName: "filemaker", - fmMcpBaseUrl: localFmMcp.baseUrl, - connectedFileName: detectedFile, - }), - catch: (cause) => - new FileSystemError({ - message: "Unable to persist local FileMaker file detection into typegen config.", - operation: "updateTypegenConfig", - path: plan.targetDir, - cause, - }), - }); - } - } - } - - if (plan.tasks.checkWebViewerAddon) { - yield* Effect.promise(async () => { - try { - const { checkForWebViewerLayouts, getWebViewerAddonMessages } = await import( - "~/installers/proofkit-webviewer.js" - ); - const status = await checkForWebViewerLayouts(plan.targetDir); - const messages = getWebViewerAddonMessages(status); - - for (const message of messages.warn) { - consoleService.warn(message); - } - for (const message of messages.info) { - consoleService.info(message); - } - if (cliContext.nonInteractive) { - additionalNextSteps.push(...messages.nextSteps); - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - consoleService.warn(`Could not inspect the ProofKit Web Viewer add-on (${message}).`); - } - }); - } - - if (plan.tasks.runInstall) { - let installArgs: string[] = ["install"]; - if (plan.request.packageManager === "yarn") { - installArgs = []; - } - const installResult = yield* Effect.either( - processService.run(plan.request.packageManager, installArgs, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }), - ); - if (installResult._tag === "Left") { - consoleService.error(renderInstallFailure(plan, installResult.left)); - return yield* Effect.fail(installResult.left); - } - } - - if (plan.tasks.runUltraciteInit) { - if (!plan.request.noInstall) { - const ultraciteCommand = getUltraciteInitCommand({ - appType: plan.request.appType, - packageManager: plan.request.packageManager, - skipInstall: plan.request.noInstall, - }); - yield* processService.run(ultraciteCommand.command, ultraciteCommand.args, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }); - } - - const oxlintConfigContent = - plan.request.appType === "browser" ? getBrowserOxlintConfig() : getWebViewerOxlintConfig(); - yield* fs.writeFile(path.join(plan.targetDir, "oxlint.config.ts"), oxlintConfigContent); - yield* fs.ensureDir(path.join(plan.targetDir, ".husky")); - yield* fs.writeFile(path.join(plan.targetDir, ".husky/pre-commit"), getHuskyPreCommitHook()); - } - - if (plan.tasks.runIntentInstall) { - const intentCommand = getIntentInstallCommand(plan.request.packageManager); - yield* processService.run(intentCommand.command, intentCommand.args, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }); - } - - if (plan.tasks.runInitialCodegen) { - yield* codegenService.runInitial(plan.targetDir, plan.request.packageManager, plan.request.proofkitToken); - } - - // plan.tasks.runFix is non-blocking: getPackageScriptCommand/processService.run can fail on fresh scaffolds. - // Effect.either also catches lint failures below and logs warnings; other errors still propagate. - if (plan.tasks.runFix) { - const fixCommand = getPackageScriptCommand(plan, "fix"); - yield* Effect.either( - processService.run(fixCommand.command, fixCommand.args, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }), - ); - } - - if (plan.tasks.runLint) { - const lintCommand = getPackageScriptCommand(plan, "lint"); - const result = yield* Effect.either( - processService.run(lintCommand.command, lintCommand.args, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }), - ); - if (result._tag === "Left") { - consoleService.warn("Lint did not succeed; continuing setup."); - } - } - - if (plan.tasks.initializeGit) { - yield* gitService.initialize(plan.targetDir); - } - - const packageManagerVersionResult = plan.request.noInstall - ? yield* Effect.either(packageManagerService.getVersion(plan.request.packageManager, plan.targetDir)) - : yield* packageManagerService.getVersion(plan.request.packageManager, plan.targetDir).pipe( - Effect.map((version) => ({ - _tag: "Right" as const, - right: version, - })), - ); - const packageManagerVersion = - packageManagerVersionResult._tag === "Right" ? packageManagerVersionResult.right : undefined; - - consoleService.success( - `Created ${plan.request.scopedAppName} in ${plan.targetDir}${ - packageManagerVersion ? ` using ${plan.request.packageManager}@${packageManagerVersion}` : "" - }`, - ); - consoleService.info(chalk.bold("Next steps:")); - consoleService.info(renderNextSteps(plan, Array.from(new Set(additionalNextSteps)))); - return plan; - }); diff --git a/packages/cli/src/core/planInit.ts b/packages/cli/src/core/planInit.ts deleted file mode 100644 index 78df8ca3..00000000 --- a/packages/cli/src/core/planInit.ts +++ /dev/null @@ -1,266 +0,0 @@ -import path from "node:path"; -import type { PackageJson } from "type-fest"; - -import { NODE_RUNTIME_VERSION } from "~/consts.js"; -import type { InitPlan, InitRequest, ProofKitSettings } from "~/core/types.js"; -import { - getFmdapiVersion, - getProofkitDependencyVersion, - getProofkitWebviewerVersion, - getTypegenVersion, -} from "~/utils/getProofKitVersion.js"; -import { - formatPackageManagerCommand, - getScaffoldVersion, - getTemplatePackageCommand, - getTemplatePackageExecuteCommand, -} from "~/utils/projectFiles.js"; -import { getNodeMajorVersion } from "~/utils/versioning.js"; - -const SHARED_PNPM_BUILD_POLICY = { - "@parcel/watcher": true, - esbuild: true, - "msgpackr-extract": true, - msw: true, - node: true, -} as const; -const NPM_PACKAGE_MANAGER_WARNING = - "Warning: We strongly suggest using PNPM 11 or greater as your package manager to better protect your computer and your app."; -const NPM_MIN_RELEASE_AGE_DAYS = 1; - -function createPackageManagerVersionRange(version: string) { - return version.startsWith("^") ? version : `^${version}`; -} - -export function createPnpmWorkspaceFileContent(appType: InitRequest["appType"]) { - const buildPolicy = { - ...SHARED_PNPM_BUILD_POLICY, - sharp: appType === "browser", - } as const; - - return [ - "# This setting defines where in the repo your apps/packages that need installed dependancies exist. This of this as a list of paths to your package.json files. ", - "packages:", - ' - "."', - "", - "allowBuilds:", - ...Object.entries(buildPolicy).map(([packageName, allowed]) => ` ${JSON.stringify(packageName)}: ${allowed}`), - "", - "trustPolicy: no-downgrade", - "", - "trustPolicyIgnoreAfter: 43200", - "", - "blockExoticSubdeps: true", - "", - ].join("\n"); -} - -export function createNpmrcFileContent() { - return [ - "# Require npm package releases to be at least 24 hours old before install.", - `min-release-age=${NPM_MIN_RELEASE_AGE_DAYS}`, - "", - ].join("\n"); -} - -function createDefaultSettings(request: InitRequest): ProofKitSettings { - return { - ui: request.ui, - appType: request.appType, - envFile: ".env", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }; -} - -function createEnvFileContent() { - return ["# When adding additional environment variables, update the schema alongside this file.", ""].join("\n"); -} - -const sharedUiDependencies = { - "@radix-ui/react-slot": "^1.2.3", - "class-variance-authority": "^0.7.1", - clsx: "^2.1.1", - "lucide-react": "^1.16.0", - "tailwind-merge": "^3.5.0", - tailwindcss: "^4.1.10", - "tw-animate-css": "^1.4.0", -} satisfies Record; - -export function planInit( - request: InitRequest, - options: { templateDir: string; packageManagerVersion?: string }, -): InitPlan { - const targetDir = path.resolve(request.cwd, request.appDir); - const proofkitFmdapiVersion = getProofkitDependencyVersion(getFmdapiVersion()); - const proofkitTypegenVersion = getProofkitDependencyVersion(getTypegenVersion()); - const proofkitWebviewerVersion = getProofkitDependencyVersion(getProofkitWebviewerVersion()); - const settings = createDefaultSettings(request); - const packageManagerCommand = getTemplatePackageCommand(request.packageManager); - const packageManagerExecuteCommand = getTemplatePackageExecuteCommand(request.packageManager); - const shouldWritePnpmWorkspaceFile = request.packageManager === "pnpm"; - const shouldWriteNpmrcFile = request.packageManager === "npm"; - - const packageJson: InitPlan["packageJson"] = { - name: request.scopedAppName, - engines: { - node: NODE_RUNTIME_VERSION, - }, - devEngines: options.packageManagerVersion - ? { - packageManager: { - name: request.packageManager, - version: createPackageManagerVersionRange(options.packageManagerVersion), - onFail: "download", - }, - runtime: { - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }, - } - : undefined, - proofkitMetadata: { - initVersion: getScaffoldVersion(), - scaffoldPackage: "@proofkit/cli", - }, - dependencies: {}, - devDependencies: { - "@types/node": `^${getNodeMajorVersion()}`, - }, - }; - - if (request.appType === "browser") { - packageJson.devDependencies["@proofkit/typegen"] = proofkitTypegenVersion; - packageJson.devDependencies.oxlint = "^1.39.0"; - Object.assign(packageJson.dependencies, sharedUiDependencies); - packageJson.dependencies["@tailwindcss/postcss"] = "^4.1.10"; - packageJson.dependencies["next-themes"] = "^0.4.6"; - if (request.dataSource === "filemaker") { - packageJson.dependencies["@proofkit/fmdapi"] = proofkitFmdapiVersion; - packageJson.dependencies.zod = "^4"; - } - } - - if (request.appType === "webviewer") { - Object.assign(packageJson.dependencies, sharedUiDependencies); - packageJson.dependencies["@proofkit/fmdapi"] = proofkitFmdapiVersion; - packageJson.dependencies["@proofkit/webviewer"] = proofkitWebviewerVersion; - packageJson.dependencies["@tanstack/react-query"] = "^5.90.21"; - packageJson.dependencies["@tanstack/react-router"] = "^1.167.4"; - packageJson.dependencies.zod = "^4"; - packageJson.devDependencies["@proofkit/typegen"] = proofkitTypegenVersion; - packageJson.devDependencies["@tailwindcss/vite"] = "^4.2.1"; - packageJson.devDependencies.oxlint = "^1.39.0"; - packageJson.devDependencies.ultracite = "^7.0.0"; - } - - const shouldRunInitialCodegen = - !request.noInstall && - request.dataSource === "filemaker" && - !request.skipFileMakerSetup && - !(request.appType === "webviewer" && request.nonInteractive && !request.hasExplicitFileMakerInputs); - - return { - request, - targetDir, - templateDir: options.templateDir, - packageManagerCommand, - packageManagerExecuteCommand, - packageJson, - settings, - envFile: { - path: path.join(targetDir, ".env"), - content: createEnvFileContent(), - }, - writes: [ - { - path: path.join(targetDir, ".cursorignore"), - content: "CLAUDE.md\n", - }, - ...(shouldWritePnpmWorkspaceFile - ? [ - { - path: path.join(targetDir, "pnpm-workspace.yaml"), - content: createPnpmWorkspaceFileContent(request.appType), - }, - ] - : []), - ...(shouldWriteNpmrcFile - ? [ - { - path: path.join(targetDir, ".npmrc"), - content: createNpmrcFileContent(), - }, - ] - : []), - ], - commands: [ - ...(request.noInstall ? [] : [{ type: "install" as const }]), - { type: "ultracite-init" as const }, - ...(request.noInstall ? [] : [{ type: "intent-install" as const }]), - ...(shouldRunInitialCodegen ? [{ type: "codegen" as const }] : []), - ...(request.noInstall ? [] : [{ type: "fix" as const }]), - ...(request.noInstall ? [] : [{ type: "lint" as const }]), - ...(request.noGit ? [] : [{ type: "git-init" as const }]), - ], - tasks: { - bootstrapFileMaker: request.dataSource === "filemaker" && !request.skipFileMakerSetup, - checkWebViewerAddon: request.appType === "webviewer", - runInstall: !request.noInstall, - runUltraciteInit: true, - runIntentInstall: !request.noInstall, - runInitialCodegen: shouldRunInitialCodegen, - runFix: !request.noInstall, - runLint: !request.noInstall, - initializeGit: !request.noGit, - }, - nextSteps: [ - `cd ${request.appDir}`, - ...(request.packageManager === "npm" ? [NPM_PACKAGE_MANAGER_WARNING] : []), - ...(request.noInstall ? [request.packageManager === "yarn" ? "yarn" : `${request.packageManager} install`] : []), - formatPackageManagerCommand(request.packageManager, "dev"), - ...(request.appType === "webviewer" - ? [ - formatPackageManagerCommand(request.packageManager, "typegen"), - formatPackageManagerCommand(request.packageManager, "launch-fm"), - ] - : []), - ], - }; -} - -export function applyPackageJsonMutations( - packageJson: PackageJson, - mutations: InitPlan["packageJson"], - overwriteDependencies = true, -) { - packageJson.name = mutations.name; - packageJson.proofkitMetadata = mutations.proofkitMetadata as PackageJson["proofkitMetadata"]; - if (mutations.devEngines) { - packageJson.devEngines = mutations.devEngines; - packageJson.packageManager = undefined; - } - packageJson.engines = mutations.engines as PackageJson["engines"]; - - if (!packageJson.dependencies) { - packageJson.dependencies = {}; - } - if (!packageJson.devDependencies) { - packageJson.devDependencies = {}; - } - - const merge = (target: Record, source: Record) => { - for (const [name, version] of Object.entries(source)) { - if (overwriteDependencies || !(name in target)) { - target[name] = version; - } - } - }; - - merge(packageJson.dependencies as Record, mutations.dependencies); - merge(packageJson.devDependencies as Record, mutations.devDependencies); - - return packageJson; -} diff --git a/packages/cli/src/core/resolveInitRequest.ts b/packages/cli/src/core/resolveInitRequest.ts deleted file mode 100644 index 6c6ae5b3..00000000 --- a/packages/cli/src/core/resolveInitRequest.ts +++ /dev/null @@ -1,801 +0,0 @@ -import { Effect } from "effect"; - -import { DEFAULT_APP_NAME } from "~/consts.js"; -import { CliContext, ConsoleService, FileMakerService, PackageManagerService, PromptService } from "~/core/context.js"; -import { - CliValidationError, - FileMakerSetupError, - isCliError, - NonInteractiveInputError, - UserCancelledError, -} from "~/core/errors.js"; -import type { AppType, CliFlags, DataSourceType, FileMakerInputs, InitRequest } from "~/core/types.js"; -import { createDataSourceEnvNames, getDefaultSchemaName } from "~/utils/projectFiles.js"; -import { parseNameAndPath, validateAppName } from "~/utils/projectName.js"; - -const defaultFlags: CliFlags = { - noGit: false, - noInstall: false, - force: false, - default: false, - CI: false, - importAlias: "~/", -}; - -function compareSemver(left: string, right: string) { - const leftParts = left.split(".").map((part) => Number.parseInt(part, 10) || 0); - const rightParts = right.split(".").map((part) => Number.parseInt(part, 10) || 0); - - for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) { - const leftValue = leftParts[index] ?? 0; - const rightValue = rightParts[index] ?? 0; - if (leftValue > rightValue) { - return 1; - } - if (leftValue < rightValue) { - return -1; - } - } - - return 0; -} - -function resolveLayoutNameMatch(layouts: string[], requestedLayoutName: string) { - const exactMatch = layouts.find((layout) => layout === requestedLayoutName); - if (exactMatch) { - return exactMatch; - } - - const normalizedRequestedLayoutName = requestedLayoutName.toLocaleLowerCase(); - return layouts.find((layout) => layout.toLocaleLowerCase() === normalizedRequestedLayoutName); -} - -function validateLayoutInputs(flags: CliFlags) { - const hasLayoutName = Boolean(flags.layoutName); - const hasSchemaName = Boolean(flags.schemaName); - - if (hasLayoutName !== hasSchemaName) { - return Effect.fail( - new CliValidationError({ - message: "Both --layout-name and --schema-name must be provided together.", - }), - ); - } - - return Effect.void; -} - -function promptEffect(message: string, run: () => Promise) { - return Effect.tryPromise({ - try: run, - catch: (cause) => - isCliError(cause) - ? cause - : new CliValidationError({ - message, - cause, - }), - }); -} - -function getMissingFlags(values: [flag: string, value: unknown][]) { - return values.filter(([, value]) => !value).map(([flag]) => flag); -} - -function createMissingInputsMessage(scope: string, flags: string[]) { - return `Missing required ${scope} inputs in non-interactive mode: ${flags.join(", ")}.`; -} - -function resolvePackageManager({ - cwd, - packageManager, - nonInteractive, -}: { - cwd: string; - packageManager: "npm" | "pnpm" | "yarn" | "bun"; - nonInteractive: boolean; -}) { - return Effect.gen(function* () { - if (packageManager !== "npm") { - return packageManager; - } - - const packageManagerService = yield* PackageManagerService; - const prompt = yield* PromptService; - const pnpmVersionResult = yield* Effect.either(packageManagerService.getVersion("pnpm", cwd)); - if (pnpmVersionResult._tag === "Right") { - return "pnpm" as const; - } - - if (nonInteractive) { - return packageManager; - } - - const packageManagerChoice = yield* promptEffect("Unable to choose package manager.", () => - prompt.select<"abort" | "continue">({ - message: - "We strongly suggest you use PNPM instead of NPM to better secure yourself and the apps you build. https://pnpm.io/installation", - options: [ - { - value: "abort", - label: "Abort", - hint: "Install PNPM first", - }, - { - value: "continue", - label: "Continue with NPM", - hint: "Ignore this warning", - }, - ], - }), - ); - - if (packageManagerChoice === "abort") { - return yield* Effect.fail( - new UserCancelledError({ - message: "User aborted to install pnpm first.", - }), - ); - } - - return packageManager; - }); -} - -function resolveHostedFileMakerInputs({ - prompt, - fileMakerService, - flags, - nonInteractive, -}: { - prompt: PromptService; - fileMakerService: FileMakerService; - flags: CliFlags; - nonInteractive: boolean; -}) { - return Effect.gen(function* () { - yield* validateLayoutInputs(flags); - - if (!flags.server && nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: createMissingInputsMessage( - "hosted FileMaker", - getMissingFlags([ - ["--server", flags.server], - ["--file-name", flags.fileName], - ["--data-api-key", flags.dataApiKey], - ]), - ), - }), - ); - } - - const rawServer = - flags.server ?? - (yield* promptEffect("Unable to read FileMaker Server URL.", () => - prompt.text({ - message: "What is the URL of your FileMaker Server?", - validate: (value) => { - try { - const normalized = value.startsWith("http") ? value : `https://${value}`; - new URL(normalized); - return; - } catch { - return "Please enter a valid URL"; - } - }, - }), - )); - - const { normalizedUrl, versions } = yield* fileMakerService.validateHostedServerUrl(rawServer); - const hostedUrl = new URL(normalizedUrl); - const demoFileName = "ProofKitDemo.fmp12"; - - let selectedFile = flags.fileName; - let dataApiKey = flags.dataApiKey; - let layoutName = flags.layoutName; - let schemaName = flags.schemaName; - let token: string | undefined; - let files: Array<{ filename: string; status: string }> = []; - - const requireHostedToken = () => - token - ? Effect.succeed(token) - : Effect.fail( - new FileMakerSetupError({ - message: "OttoFMS authentication is required for hosted setup.", - }), - ); - - if (!(selectedFile && dataApiKey)) { - if (!(flags.adminApiKey || (versions.ottoVersion && compareSemver(versions.ottoVersion, "4.7.0") >= 0))) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: - "OttoFMS 4.7.0 or later is required to auto-login. Upgrade OttoFMS or pass --admin-api-key for hosted setup.", - }), - ); - } - token = flags.adminApiKey ?? (yield* fileMakerService.getOttoFMSToken({ url: hostedUrl })).token; - } - - if (!selectedFile) { - if (nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: createMissingInputsMessage( - "FileMaker", - getMissingFlags([ - ["--file-name", selectedFile], - ["--data-api-key", dataApiKey], - ]), - ), - }), - ); - } - - files = yield* fileMakerService.listFiles({ - url: hostedUrl, - token: yield* requireHostedToken(), - }); - selectedFile = yield* promptEffect("Unable to choose a FileMaker file.", () => - prompt.searchSelect({ - message: "Which file would you like to connect to?", - options: [ - { - value: "$deploy-demo", - label: "Deploy NEW ProofKit Demo File", - hint: "Use OttoFMS to deploy a new file for testing", - keywords: ["demo", "proofkit"], - }, - ...files - .slice() - .sort((left, right) => left.filename.localeCompare(right.filename)) - .map((file) => ({ - value: file.filename, - label: file.filename, - hint: file.status, - keywords: [file.filename], - })), - ], - }), - ); - } - - if (!selectedFile) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker file was selected.", - }), - ); - } - - if (selectedFile === "$deploy-demo") { - if (files.length === 0) { - files = yield* fileMakerService.listFiles({ - url: hostedUrl, - token: yield* requireHostedToken(), - }); - } - const demoExists = files.some((file) => file.filename === demoFileName); - const replaceDemo = - demoExists && !nonInteractive - ? yield* promptEffect("Unable to confirm ProofKit Demo replacement.", () => - prompt.confirm({ - message: "The demo file already exists. Do you want to replace it with a fresh copy?", - initialValue: false, - }), - ) - : demoExists; - const deployed = yield* fileMakerService.deployDemoFile({ - url: hostedUrl, - token: yield* requireHostedToken(), - operation: replaceDemo ? "replace" : "install", - }); - selectedFile = deployed.filename; - dataApiKey = deployed.apiKey; - layoutName ??= "API_Contacts"; - schemaName ??= "Contacts"; - } - - if (!dataApiKey && nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: createMissingInputsMessage("FileMaker", getMissingFlags([["--data-api-key", dataApiKey]])), - }), - ); - } - - if (!dataApiKey) { - const apiKeys = (yield* fileMakerService.listAPIKeys({ - url: hostedUrl, - token: yield* requireHostedToken(), - })).filter((apiKey: { database: string }) => apiKey.database === selectedFile); - - const selection = - apiKeys.length === 0 - ? "create" - : yield* promptEffect("Unable to choose an OttoFMS Data API key.", () => - prompt.searchSelect({ - message: "Which OttoFMS Data API key would you like to use?", - options: [ - ...apiKeys.map((apiKey: { key: string; label: string; user: string; database: string }) => ({ - value: apiKey.key, - label: `${apiKey.label} - ${apiKey.user}`, - hint: `${apiKey.key.slice(0, 5)}...${apiKey.key.slice(-4)}`, - keywords: [apiKey.label, apiKey.user, apiKey.database], - })), - { - value: "create", - label: "Create a new API key", - hint: "Requires FileMaker credentials for this file", - keywords: ["create", "new"], - }, - ], - }), - ); - - if (selection === "create") { - const username = yield* promptEffect("Unable to read FileMaker account name.", () => - prompt.text({ - message: `Enter the account name for ${selectedFile}`, - validate: (value) => (value ? undefined : "An account name is required"), - }), - ); - const password = yield* promptEffect("Unable to read FileMaker account password.", () => - prompt.password({ - message: `Enter the password for ${username}`, - validate: (value) => (value ? undefined : "A password is required"), - }), - ); - if (!selectedFile) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker file was selected.", - }), - ); - } - dataApiKey = (yield* fileMakerService.createDataAPIKeyWithCredentials({ - url: hostedUrl, - filename: selectedFile, - username, - password, - })).apiKey; - } else { - dataApiKey = selection; - } - } - - if (!dataApiKey) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker Data API key was selected.", - }), - ); - } - - const resolvedFileName = selectedFile; - if (!resolvedFileName) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker file was selected.", - }), - ); - } - - const layouts = yield* fileMakerService.listLayouts({ - dataApiKey, - fmFile: resolvedFileName, - server: hostedUrl.origin, - }); - - if (layoutName) { - const matchedLayoutName = resolveLayoutNameMatch(layouts, layoutName); - if (!matchedLayoutName) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: `Layout "${layoutName}" was not found in ${resolvedFileName}.`, - }), - ); - } - - layoutName = matchedLayoutName; - } - - if (!(nonInteractive || layoutName || schemaName)) { - const shouldConfigureLayout = yield* promptEffect("Unable to confirm initial layout setup.", () => - prompt.confirm({ - message: "Do you want to configure an initial layout for type generation now?", - initialValue: false, - }), - ); - - if (shouldConfigureLayout) { - layoutName = yield* promptEffect("Unable to choose a FileMaker layout.", () => - prompt.searchSelect({ - message: "Select a layout to read data from", - options: layouts.map((layout: string) => ({ - value: layout, - label: layout, - keywords: [layout], - })), - }), - ); - - const resolvedLayoutName = layoutName; - if (!resolvedLayoutName) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker layout was selected.", - }), - ); - } - schemaName = yield* promptEffect("Unable to read generated schema name.", () => - prompt.text({ - message: "What should the generated schema be called?", - defaultValue: getDefaultSchemaName(resolvedLayoutName), - validate: (value) => (value ? undefined : "A schema name is required"), - }), - ); - } - } - - if (!selectedFile) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker file was selected.", - }), - ); - } - if (!dataApiKey) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker Data API key was selected.", - }), - ); - } - - return { - mode: "hosted-otto", - dataSourceName: "filemaker", - envNames: createDataSourceEnvNames("filemaker"), - server: hostedUrl.origin, - fileName: selectedFile, - dataApiKey, - layoutName, - schemaName, - adminApiKey: flags.adminApiKey, - fmsVersion: versions.fmsVersion, - ottoVersion: versions.ottoVersion, - } satisfies FileMakerInputs; - }); -} - -function resolveFileMakerInputs({ - prompt, - console, - fileMakerService, - flags, - appType, - nonInteractive, - projectName, - proofkitToken, -}: { - prompt: PromptService; - console: ConsoleService; - fileMakerService: FileMakerService; - flags: CliFlags; - appType: AppType; - nonInteractive: boolean; - projectName: string; - proofkitToken?: string; -}) { - return Effect.gen(function* () { - if (flags.dataSource !== "filemaker") { - return { fileMaker: undefined, skipFileMakerSetup: false }; - } - - yield* validateLayoutInputs(flags); - - if (appType === "webviewer" && !flags.server) { - const resolveLocalFmMcpFile = (connectedFiles: string[]) => - Effect.gen(function* () { - const availableFiles = connectedFiles.filter(Boolean); - if (availableFiles.length === 0) { - return undefined; - } - - if (flags.fileName) { - if (availableFiles.includes(flags.fileName)) { - return flags.fileName; - } - - return yield* Effect.fail( - new FileMakerSetupError({ - message: `FileMaker file "${flags.fileName}" is not currently connected to the ProofKit plugin. Connected files: ${availableFiles.join(", ")}.`, - }), - ); - } - - if (availableFiles.length === 1) { - return availableFiles[0]; - } - - if (nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: `Multiple FileMaker files are connected to the ProofKit plugin. Pass --file-name with one of: ${availableFiles.join(", ")}.`, - }), - ); - } - - return yield* promptEffect("Unable to choose a local FileMaker file.", () => - prompt.searchSelect({ - message: "Multiple FileMaker files are open. Which file should ProofKit use?", - options: availableFiles.map((fileName) => ({ - value: fileName, - label: fileName, - hint: "Connected via ProofKit plugin", - keywords: [fileName], - })), - }), - ); - }); - - while (true) { - const localFmMcp = yield* fileMakerService.detectLocalFmMcp(); - yield* fileMakerService.installLocalWebViewerAddon(); - const selectedFile = localFmMcp.healthy ? yield* resolveLocalFmMcpFile(localFmMcp.connectedFiles) : undefined; - if (localFmMcp.healthy && selectedFile) { - if (!(nonInteractive || proofkitToken)) { - yield* fileMakerService.authorizeLocalFmMcp({ - baseUrl: localFmMcp.baseUrl, - fileName: selectedFile, - interactive: true, - clientName: `ProofKit CLI (${projectName})`, - clientDescription: - "ProofKit CLI wants to read layouts from your FileMaker file to help set up your project.", - }); - } - console.info(`Using ProofKit plugin file: ${selectedFile}`); - return { - fileMaker: { - mode: "local-fm-mcp", - dataSourceName: "filemaker", - envNames: createDataSourceEnvNames("filemaker"), - fmMcpBaseUrl: localFmMcp.baseUrl, - fileName: selectedFile, - layoutName: flags.layoutName, - schemaName: flags.schemaName, - proofkitToken, - } satisfies FileMakerInputs, - skipFileMakerSetup: false, - }; - } - - if (nonInteractive) { - if (localFmMcp.healthy) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: - "ProofKit plugin was detected, but no FileMaker file is connected. Install the ProofKit plugin, install the ProofKit Web Viewer add-on in your FileMaker file, then run the add-on connection script and rerun. Or pass --server.", - }), - ); - } - - return yield* Effect.fail( - new NonInteractiveInputError({ - message: - "ProofKit plugin was not detected and no FileMaker server was provided. Install the ProofKit plugin, then rerun. Or pass --server.", - }), - ); - } - - const fallbackAction = yield* promptEffect("Unable to choose FileMaker setup fallback.", () => - prompt.select({ - message: localFmMcp.healthy - ? "ProofKit plugin is installed, but no FileMaker file is connected yet. Install the ProofKit Web Viewer add-on in your FileMaker file, run the add-on connection script, then choose how to continue." - : "ProofKit plugin was not detected. How would you like to continue?", - options: [ - { - value: "retry", - label: "Try again", - hint: localFmMcp.healthy - ? "Check again after opening a FileMaker file" - : "Retry ProofKit plugin detection", - }, - { - value: "hosted", - label: "Continue with hosted setup", - hint: "Use OttoFMS and a hosted FileMaker server", - }, - { - value: "skip", - label: "Skip for now", - hint: "Create the project and configure FileMaker later", - }, - ], - }), - ); - - if (fallbackAction === "retry") { - continue; - } - - if (fallbackAction === "skip") { - return { - fileMaker: undefined, - skipFileMakerSetup: true, - }; - } - - break; - } - } - - return { - fileMaker: yield* resolveHostedFileMakerInputs({ - prompt, - fileMakerService, - flags, - nonInteractive, - }), - skipFileMakerSetup: false, - }; - }); -} - -export const resolveInitRequest = (name?: string, rawFlags?: CliFlags) => - Effect.gen(function* () { - const flags = { ...defaultFlags, ...rawFlags }; - const proofkitToken = flags.proofkitToken?.trim() || process.env.FM_MCP_SESSION_ID?.trim(); - const prompt = yield* PromptService; - const console = yield* ConsoleService; - const fileMakerService = yield* FileMakerService; - const cliContext = yield* CliContext; - const nonInteractive = cliContext.nonInteractive || flags.CI || flags.nonInteractive === true; - const packageManager = yield* resolvePackageManager({ - cwd: cliContext.cwd, - packageManager: cliContext.packageManager, - nonInteractive, - }); - - let projectName = name; - if (!projectName) { - if (nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: "Project name is required in non-interactive mode.", - }), - ); - } - - projectName = yield* promptEffect("Unable to read project name.", () => - prompt.text({ - message: "What will your project be called?", - defaultValue: DEFAULT_APP_NAME, - validate: validateAppName, - }), - ); - } - - if (!projectName) { - return yield* Effect.fail( - new CliValidationError({ - message: "Project name is required.", - }), - ); - } - - const validationError = validateAppName(projectName); - if (validationError) { - return yield* Effect.fail( - new CliValidationError({ - message: validationError, - }), - ); - } - - let appType: AppType = flags.appType ?? "browser"; - if (!(flags.appType || nonInteractive)) { - appType = yield* promptEffect("Unable to choose app type.", () => - prompt.select({ - message: "What kind of app do you want to build?", - options: [ - { - value: "browser", - label: "Web App for Browsers", - hint: "Uses Next.js and hosted deployment", - }, - { - value: "webviewer", - label: "FileMaker Web Viewer", - hint: "Uses Vite for FileMaker web viewers", - }, - ], - }), - ); - } - - const hasExplicitFileMakerInputs = Boolean( - flags.server || flags.adminApiKey || flags.dataApiKey || flags.fileName || flags.layoutName || flags.schemaName, - ); - - let dataSource: DataSourceType = "none"; - if (flags.dataSource) { - dataSource = flags.dataSource; - } else if (appType === "webviewer") { - dataSource = hasExplicitFileMakerInputs || !(nonInteractive && !flags.server) ? "filemaker" : "none"; - } - - if (!(nonInteractive || flags.dataSource) && appType !== "webviewer") { - dataSource = yield* promptEffect("Unable to choose data source setup.", () => - prompt.select({ - message: "Do you want to connect to a FileMaker Database now?", - options: [ - { - value: "filemaker", - label: "Yes", - hint: "Set up env, datasource config, and typegen now", - }, - { - value: "none", - label: "No", - hint: "You can add a data source later", - }, - ], - }), - ); - } - - if (nonInteractive && !flags.dataSource && hasExplicitFileMakerInputs) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: "FileMaker flags require --data-source filemaker in non-interactive mode.", - }), - ); - } - - if (nonInteractive && dataSource !== "filemaker" && hasExplicitFileMakerInputs) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: "FileMaker flags require --data-source filemaker in non-interactive mode.", - }), - ); - } - - const [scopedAppName, appDir] = parseNameAndPath(projectName); - - const { fileMaker, skipFileMakerSetup } = yield* resolveFileMakerInputs({ - prompt, - console, - fileMakerService, - flags: { ...flags, dataSource }, - appType, - nonInteractive, - projectName: scopedAppName, - proofkitToken, - }); - - return { - projectName, - scopedAppName, - appDir, - appType, - ui: flags.ui ?? "shadcn", - dataSource, - packageManager, - noInstall: flags.noInstall, - noGit: flags.noGit, - force: flags.force, - cwd: cliContext.cwd, - importAlias: flags.importAlias, - nonInteractive, - debug: cliContext.debug, - proofkitToken, - fileMaker, - skipFileMakerSetup, - hasExplicitFileMakerInputs, - } satisfies InitRequest; - }); diff --git a/packages/cli/src/core/types.ts b/packages/cli/src/core/types.ts deleted file mode 100644 index bd49f71e..00000000 --- a/packages/cli/src/core/types.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { PackageManager } from "~/utils/packageManager.js"; - -export type AppType = "browser" | "webviewer"; -export type UIType = "shadcn" | "mantine"; -export type DataSourceType = "filemaker" | "none"; -export type OverwriteMode = "overwrite" | "clear"; -export type FileMakerMode = "hosted-otto" | "local-fm-mcp"; - -export interface CliFlags { - noGit: boolean; - noInstall: boolean; - force: boolean; - default: boolean; - importAlias: string; - debug?: boolean; - server?: string; - adminApiKey?: string; - fileName?: string; - layoutName?: string; - schemaName?: string; - dataApiKey?: string; - auth?: "none"; - dataSource?: DataSourceType; - ui?: UIType; - CI: boolean; - nonInteractive?: boolean; - appType?: AppType; - proofkitToken?: string; -} - -export interface FileMakerEnvNames { - database: string; - server: string; - apiKey: string; -} - -export interface HostedFileMakerInputs { - mode: "hosted-otto"; - dataSourceName: string; - envNames: FileMakerEnvNames; - server: string; - fileName: string; - dataApiKey: string; - layoutName?: string; - schemaName?: string; - adminApiKey?: string; - fmsVersion?: string; - ottoVersion?: string | null; -} - -export interface LocalFmMcpInputs { - mode: "local-fm-mcp"; - dataSourceName: string; - envNames: FileMakerEnvNames; - fmMcpBaseUrl: string; - fileName: string; - layoutName?: string; - schemaName?: string; - proofkitToken?: string; -} - -export type FileMakerInputs = HostedFileMakerInputs | LocalFmMcpInputs; - -export interface InitRequest { - projectName: string; - scopedAppName: string; - appDir: string; - appType: AppType; - ui: UIType; - dataSource: DataSourceType; - packageManager: PackageManager; - noInstall: boolean; - noGit: boolean; - force: boolean; - cwd: string; - importAlias: string; - nonInteractive: boolean; - debug: boolean; - proofkitToken?: string; - skipFileMakerSetup: boolean; - fileMaker?: FileMakerInputs; - hasExplicitFileMakerInputs: boolean; -} - -export interface ProofKitSettings { - ui: UIType; - appType: AppType; - envFile?: string; - dataSources: Array<{ - type: "fm"; - name: string; - envNames: { - database: string; - server: string; - apiKey: string; - }; - }>; - replacedMainPage: boolean; - registryTemplates: string[]; -} - -export interface InitPlan { - request: InitRequest; - targetDir: string; - templateDir: string; - overwriteMode?: OverwriteMode; - packageManagerCommand: string; - packageManagerExecuteCommand: string; - packageJson: { - name: string; - devEngines?: { - packageManager: { - name: PackageManager; - version: string; - onFail: "download"; - }; - runtime: { - name: "node"; - version: string; - onFail: "download"; - }; - }; - engines: { - node: string; - }; - proofkitMetadata: { - initVersion: string; - scaffoldPackage: "@proofkit/cli"; - }; - dependencies: Record; - devDependencies: Record; - }; - settings: ProofKitSettings; - envFile: { - path: string; - content: string; - }; - writes: Array<{ - path: string; - content: string; - }>; - commands: Array< - | { type: "install" } - | { type: "ultracite-init" } - | { type: "intent-install" } - | { type: "codegen" } - | { type: "fix" } - | { type: "lint" } - | { type: "git-init" } - >; - tasks: { - bootstrapFileMaker: boolean; - checkWebViewerAddon: boolean; - runInstall: boolean; - runUltraciteInit: boolean; - runIntentInstall: boolean; - runInitialCodegen: boolean; - runFix: boolean; - runLint: boolean; - initializeGit: boolean; - }; - nextSteps: string[]; -} - -export interface InitResult { - request: InitRequest; - plan: InitPlan; -} diff --git a/packages/cli/src/helpers/intent.ts b/packages/cli/src/helpers/intent.ts deleted file mode 100644 index 83c6a139..00000000 --- a/packages/cli/src/helpers/intent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { PackageManager } from "~/utils/packageManager.js"; -import { getTemplatePackageExecuteCommand, parseCommandString } from "~/utils/projectFiles.js"; - -function splitExecuteCommand(packageManager: PackageManager) { - const [command, ...args] = parseCommandString(getTemplatePackageExecuteCommand(packageManager)); - if (!command) { - throw new Error(`Unable to resolve package execute command for ${packageManager}.`); - } - return { command, args }; -} - -export function getIntentInstallCommand(packageManager: PackageManager) { - const execute = splitExecuteCommand(packageManager); - return { - command: execute.command, - args: [...execute.args, "@tanstack/intent@latest", "install"], - }; -} diff --git a/packages/cli/src/helpers/ultracite.ts b/packages/cli/src/helpers/ultracite.ts deleted file mode 100644 index 463bbd71..00000000 --- a/packages/cli/src/helpers/ultracite.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { AppType } from "~/core/types.js"; -import type { PackageManager } from "~/utils/packageManager.js"; -import { getTemplatePackageExecuteCommand, parseCommandString } from "~/utils/projectFiles.js"; - -const ULTRACITE_EDITORS = ["cursor"] as const; -const ULTRACITE_AGENTS = ["claude", "codex"] as const; -const ULTRACITE_HOOKS = ["cursor", "windsurf"] as const; -const ULTRACITE_INIT_PACKAGE = "ultracite@^7"; - -function splitExecuteCommand(packageManager: PackageManager) { - const [command, ...args] = parseCommandString(getTemplatePackageExecuteCommand(packageManager)); - if (!command) { - throw new Error(`Unable to resolve package execute command for ${packageManager}.`); - } - return { command, args }; -} - -export function getUltraciteFrameworks(appType: AppType) { - return appType === "browser" ? ["react", "next"] : ["react"]; -} - -export function getUltraciteInitCommand({ - appType, - packageManager, - skipInstall, -}: { - appType: AppType; - packageManager: PackageManager; - skipInstall: boolean; -}) { - const execute = splitExecuteCommand(packageManager); - return { - command: execute.command, - args: [ - ...execute.args, - ULTRACITE_INIT_PACKAGE, - "init", - "--quiet", - "--linter", - "oxlint", - "--pm", - packageManager, - "--frameworks", - ...getUltraciteFrameworks(appType), - "--editors", - ...ULTRACITE_EDITORS, - "--agents", - ...ULTRACITE_AGENTS, - "--hooks", - ...ULTRACITE_HOOKS, - ...(skipInstall ? ["--skip-install"] : []), - ], - }; -} - -export function getBrowserOxlintConfig() { - return `import { defineConfig } from "oxlint"; -import core from "ultracite/oxlint/core"; -import next from "ultracite/oxlint/next"; -import react from "ultracite/oxlint/react"; - -export default defineConfig({ -\textends: [core, react, next], -\trules: { -\t\t"func-style": "off", -\t\t"next/no-img-element": "off", -\t\t"promise/prefer-await-to-then": "off", -\t\t"promise/prefer-catch": "off", -\t\t"unicorn/filename-case": "off", -\t}, -}); -`; -} - -export function getWebViewerOxlintConfig() { - return `import { defineConfig } from "oxlint"; -import core from "ultracite/oxlint/core"; -import react from "ultracite/oxlint/react"; - -export default defineConfig({ -\textends: [core, react], -\trules: { -\t\t"react/react-in-jsx-scope": "off", -\t}, -}); -`; -} - -export function getHuskyPreCommitHook() { - return `#!/bin/sh -echo "Running lint-staged..." -pnpm exec lint-staged -`; -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts deleted file mode 100644 index d40edbba..00000000 --- a/packages/cli/src/index.ts +++ /dev/null @@ -1,423 +0,0 @@ -#!/usr/bin/env node -import { realpathSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { optional as optionalArg, text as textArg, withDescription as withArgDescription } from "@effect/cli/Args"; -import { - make as makeCommand, - run, - withDescription as withCommandDescription, - withSubcommands, -} from "@effect/cli/Command"; -import { - boolean as booleanOption, - choice as choiceOption, - optional as optionalOption, - text as textOption, - withAlias, - withDescription as withOptionDescription, -} from "@effect/cli/Options"; -import { isValidationError } from "@effect/cli/ValidationError"; -import { layer as nodeContextLayer } from "@effect/platform-node/NodeContext"; -import { Cause, Effect, Exit } from "effect"; -import { getOrUndefined } from "effect/Option"; -import { cliName } from "~/consts.js"; -import { - CliContext, - ConsoleService, - FileSystemService, - PackageManagerService, - PromptService, - TemplateService, -} from "~/core/context.js"; -import { runDoctor } from "~/core/doctor.js"; -import { getCliErrorMessage, isCliError, NonInteractiveInputError, UserCancelledError } from "~/core/errors.js"; -import { executeInitPlan } from "~/core/executeInitPlan.js"; -import { planInit } from "~/core/planInit.js"; -import { resolveInitRequest } from "~/core/resolveInitRequest.js"; -import type { CliFlags } from "~/core/types.js"; -import { CLI_VERSION } from "~/package-versions.js"; -import { makeLiveLayer } from "~/services/live.js"; -import { resolveNonInteractiveMode } from "~/utils/nonInteractive.js"; -import { intro } from "~/utils/prompts.js"; -import { proofGradient, renderTitle } from "~/utils/renderTitle.js"; - -const defaultCliFlags: CliFlags = { - noGit: false, - noInstall: false, - force: false, - default: false, - CI: false, - importAlias: "~/", -}; - -function getCliVersion() { - return CLI_VERSION; -} - -export const runInit = (name?: string, rawFlags?: Partial) => - Effect.gen(function* () { - const templateService = yield* TemplateService; - const packageManagerService = yield* PackageManagerService; - const request = yield* resolveInitRequest(name, { - ...defaultCliFlags, - ...rawFlags, - }); - const templateDir = templateService.getTemplateDir(request.appType, request.ui); - const packageManagerVersionResult = request.noInstall - ? yield* Effect.either(packageManagerService.getVersion(request.packageManager, request.cwd)) - : yield* packageManagerService.getVersion(request.packageManager, request.cwd).pipe( - Effect.map((version) => ({ - _tag: "Right" as const, - right: version, - })), - ); - const packageManagerVersion = - packageManagerVersionResult._tag === "Right" ? packageManagerVersionResult.right : undefined; - const plan = planInit(request, { templateDir, packageManagerVersion }); - yield* executeInitPlan(plan); - return { request, plan }; - }); - -type ProjectMenuChoice = "typegen" | "doctor" | "docs"; - -function isPromptCancellationError(error: unknown) { - return error instanceof UserCancelledError || (error instanceof Error && error.name === "ExitPromptError"); -} - -function toProjectMenuCommandError(command: string, cause: unknown) { - if (isCliError(cause)) { - return cause; - } - - const error = new Error(`Failed to run \`${command}\` from project menu.`); - Object.assign(error, { cause }); - return error; -} - -const runProjectMenu = Effect.gen(function* () { - const prompt = yield* PromptService; - const consoleService = yield* ConsoleService; - - const menuChoice = yield* Effect.tryPromise({ - try: () => - prompt.select({ - message: "What would you like to do?", - options: [ - { - label: "Generate Types", - value: "typegen", - hint: "Update field definitions from your data sources", - }, - { - label: "Doctor", - value: "doctor", - hint: "Inspect project health and next steps", - }, - { - label: "View Documentation", - value: "docs", - hint: "Open ProofKit documentation", - }, - ], - }), - catch: (cause) => - isPromptCancellationError(cause) - ? new UserCancelledError({ message: "User aborted the operation" }) - : toProjectMenuCommandError("menu selection", cause), - }); - - switch (menuChoice) { - case "typegen": - return yield* Effect.promise(async () => { - const { runTypegen } = await import("~/cli/typegen/index.js"); - await runTypegen(); - }); - case "doctor": - return yield* runDoctor; - case "docs": { - const { DOCS_URL } = yield* Effect.promise(() => import("~/consts.js")); - consoleService.info(`Opening ${DOCS_URL} in your browser...`); - const { default: open } = yield* Effect.promise(() => import("open")); - yield* Effect.promise(() => open(DOCS_URL)); - return; - } - default: - throw new Error(`Unknown menu choice: ${menuChoice}`); - } -}); - -export const runDefaultCommand = (rawFlags?: Partial) => - Effect.gen(function* () { - const cliContext = yield* CliContext; - const fsService = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const flags = { ...defaultCliFlags, ...rawFlags }; - const settingsPath = path.join(cliContext.cwd, "proofkit.json"); - const hasProofKitProject = yield* fsService.exists(settingsPath); - - if (hasProofKitProject) { - intro(`Found ${proofGradient("ProofKit")} project`); - if (!(cliContext.nonInteractive || flags.CI || flags.nonInteractive)) { - return yield* runProjectMenu; - } - - consoleService.note( - [ - "ProofKit now focuses on project bootstrap and diagnostics.", - "Use an explicit command such as `proofkit doctor`, `proofkit typegen`, or `proofkit init`.", - ].join("\n"), - "Project commands", - ); - return; - } - - if (cliContext.nonInteractive || flags.CI || flags.nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: - "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit init --non-interactive`.", - }), - ); - } - - intro(`No ${proofGradient("ProofKit")} project found, running \`init\``); - yield* runInit(undefined, { - ...flags, - default: true, - }); - }); - -const initDirectoryArg = optionalArg(textArg({ name: "dir" })).pipe( - withArgDescription("The project name or target directory. Use `.` for the current directory, best when it is empty."), -); - -function optionalTextOption(name: string, description: string) { - return optionalOption(textOption(name).pipe(withOptionDescription(description))); -} - -function optionalChoiceOption(name: string, choices: Choices, description: string) { - return optionalOption(choiceOption(name, choices).pipe(withOptionDescription(description))); -} - -function getCurrentTTYState() { - return { - stdinIsTTY: process.stdin?.isTTY, - stdoutIsTTY: process.stdout?.isTTY, - }; -} - -function makeInitCommand() { - return makeCommand( - "init", - { - dir: initDirectoryArg, - appType: optionalChoiceOption("app-type", ["browser", "webviewer"] as const, "The type of app to create"), - server: optionalTextOption("server", "The URL of your FileMaker Server"), - adminApiKey: optionalTextOption("admin-api-key", "Admin API key for OttoFMS"), - fileName: optionalTextOption( - "file-name", - "The FileMaker file name to use, including selecting a local connected file", - ), - layoutName: optionalTextOption("layout-name", "The FileMaker layout name to scaffold"), - schemaName: optionalTextOption("schema-name", "The generated schema name"), - dataApiKey: optionalTextOption("data-api-key", "The Otto Data API key to use"), - proofkitToken: optionalTextOption( - "proofkit-token", - "ProofKit session token to pass through to local FileMaker MCP setup", - ), - dataSource: optionalChoiceOption("data-source", ["filemaker", "none"] as const, "The data source to use"), - noGit: booleanOption("no-git").pipe(withOptionDescription("Skip git initialization")), - noInstall: booleanOption("no-install").pipe(withOptionDescription("Skip package installation")), - force: booleanOption("force").pipe( - withAlias("f"), - withOptionDescription("Force overwrite target directory when it already contains files"), - ), - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ dir, ...options }) => { - const nonInteractive = resolveNonInteractiveMode({ - CI: options.CI, - nonInteractive: options.nonInteractive, - ...getCurrentTTYState(), - }); - - const flags: CliFlags = { - ...defaultCliFlags, - appType: getOrUndefined(options.appType), - server: getOrUndefined(options.server), - adminApiKey: getOrUndefined(options.adminApiKey), - fileName: getOrUndefined(options.fileName), - layoutName: getOrUndefined(options.layoutName), - schemaName: getOrUndefined(options.schemaName), - dataApiKey: getOrUndefined(options.dataApiKey), - proofkitToken: getOrUndefined(options.proofkitToken), - dataSource: getOrUndefined(options.dataSource), - noGit: options.noGit, - noInstall: options.noInstall, - force: options.force, - CI: options.CI, - nonInteractive: options.nonInteractive, - debug: options.debug, - }; - - return makeLiveLayer({ - cwd: process.cwd(), - debug: flags.debug === true, - nonInteractive, - })(runInit(getOrUndefined(dir), flags)); - }, - ).pipe(withCommandDescription("Create a new project with ProofKit")); -} - -function makeTypegenCommand() { - return makeCommand( - "typegen", - { - config: optionalTextOption("config", "Optional typegen config file name"), - envPath: optionalTextOption("env-path", "Optional path to your .env file"), - proofkitToken: optionalTextOption("proofkit-token", "Transient ProofKit token for FM MCP authorization"), - resetOverrides: booleanOption("reset-overrides").pipe( - withOptionDescription("Recreate the overrides file(s) even if they already exist"), - ), - }, - ({ config, envPath, proofkitToken, resetOverrides }) => - Effect.promise(async () => { - const { runTypegen } = await import("~/cli/typegen/index.js"); - await runTypegen({ - config: getOrUndefined(config), - envPath: getOrUndefined(envPath), - proofkitToken: getOrUndefined(proofkitToken), - resetOverrides, - }); - }), - ).pipe(withCommandDescription("Generate types via @proofkit/typegen")); -} - -function makeDoctorCommand() { - return makeCommand( - "doctor", - { - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ debug }) => - makeLiveLayer({ - cwd: process.cwd(), - debug: debug === true, - nonInteractive: true, - })(runDoctor), - ).pipe(withCommandDescription("Inspect project health and suggest exact next steps")); -} - -const rootCommand = makeCommand( - cliName, - { - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - (options) => - makeLiveLayer({ - cwd: process.cwd(), - debug: options.debug === true, - nonInteractive: resolveNonInteractiveMode({ - CI: options.CI, - nonInteractive: options.nonInteractive, - ...getCurrentTTYState(), - }), - })( - runDefaultCommand({ - ...defaultCliFlags, - CI: options.CI, - nonInteractive: options.nonInteractive, - debug: options.debug, - }), - ), -).pipe( - withCommandDescription("Interactive CLI to scaffold and manage ProofKit projects"), - withSubcommands([makeInitCommand(), makeDoctorCommand(), makeTypegenCommand()]), -); - -export const cli = run(rootCommand, { - name: "ProofKit", - version: getCliVersion(), -}); - -function isMainEntrypoint(argvPath: string | undefined, moduleUrl: string) { - if (!argvPath) { - return false; - } - - const resolvedModulePath = fileURLToPath(moduleUrl); - - try { - return realpathSync(argvPath) === realpathSync(resolvedModulePath); - } catch { - return path.resolve(argvPath) === path.resolve(resolvedModulePath); - } -} - -const isMainModule = isMainEntrypoint(process.argv[1], import.meta.url); - -const debugFlagNames = new Set(["--debug"]); -const versionFlagNames = new Set(["-v", "--version"]); - -function shouldShowDebugDetails(argv: readonly string[]) { - return argv.some((arg) => debugFlagNames.has(arg)); -} - -function isVersionRequest(argv: readonly string[]) { - const args = argv.slice(2); - return args.length === 1 && versionFlagNames.has(args[0] ?? ""); -} - -export function renderFailure(cause: Cause.Cause, showDebugDetails: boolean) { - const failure = getOrUndefined(Cause.failureOption(cause)); - - if (failure && isValidationError(failure)) { - if (showDebugDetails) { - console.error(`\n[debug] ${Cause.pretty(cause)}`); - } - return; - } - - if (failure && isCliError(failure)) { - console.error(getCliErrorMessage(failure)); - } else { - const error = Cause.squash(cause); - console.error(error instanceof Error ? error.message : String(error)); - } - - if (showDebugDetails) { - console.error(`\n[debug] ${Cause.pretty(cause)}`); - } -} - -async function main(argv: readonly string[]) { - const showDebugDetails = shouldShowDebugDetails(argv); - const exit = await Effect.runPromiseExit(Effect.provide(cli(argv), nodeContextLayer)); - - if (Exit.isFailure(exit)) { - renderFailure(exit.cause, showDebugDetails); - process.exitCode = 1; - } -} - -if (isMainModule) { - if (isVersionRequest(process.argv)) { - console.log(getCliVersion()); - process.exit(0); - } - - renderTitle(getCliVersion()); - main(process.argv).catch((error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exitCode = 1; - }); -} diff --git a/packages/cli/src/installers/install-fm-addon.ts b/packages/cli/src/installers/install-fm-addon.ts deleted file mode 100644 index 621c6e00..00000000 --- a/packages/cli/src/installers/install-fm-addon.ts +++ /dev/null @@ -1,404 +0,0 @@ -import crypto from "node:crypto"; -import os from "node:os"; -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; - -import { openExternal } from "~/utils/browserOpen.js"; -import { requestArrayBuffer, requestJson } from "~/utils/http.js"; - -export type FmAddonName = "auth" | "wv"; -export type FmAddonInspectionStatus = "missing" | "installed-current" | "installed-outdated" | "unknown"; - -export interface FmAddonInspection { - status: FmAddonInspectionStatus; - addonName: FmAddonName; - addonDir: string; - addonDisplayName: string; - targetDir: string | null; - installedPath: string | null; - remoteAssetUrl: string; - latestVersion?: string; - installedVersion?: string; - reason?: string; -} - -type FmAddonTarget = "webviewer" | "auth"; - -interface FmAddonManifestAsset { - file?: string; - url?: string; - sha256?: string; - size?: number; -} - -interface FmAddonManifestEntry { - version?: string; - latestVersion?: string; - assets?: FmAddonManifestAsset[]; - url?: string; - file?: string; -} - -interface FmAddonManifest { - product?: string; - updatedAt?: string; - latestVersion?: string; - addons?: Partial>; - versions?: Array<{ - version?: string; - assets?: FmAddonManifestAsset[]; - addons?: Partial>; - }>; -} - -const DEFAULT_FM_ADDON_MANIFEST_URL = "https://downloads.ottomatic.cloud/proofkit/manifest.json"; -const FM_ADDON_VERSION_REGEX = /]*\bversion="([^"]+)"/i; -const NUMERIC_VERSION_PART_REGEX = /^\d+$/; - -function getAddonDisplayName(addonName: FmAddonName) { - return addonName === "auth" ? "FM Auth Add-on" : "ProofKit Web Viewer"; -} - -function getAddonDir(addonName: FmAddonName) { - return addonName === "auth" ? "ProofKitAuth" : "ProofKitWV"; -} - -function getAddonTarget(addonName: FmAddonName): FmAddonTarget { - return addonName === "auth" ? "auth" : "webviewer"; -} - -function getAddonDocsUrl(addonName: FmAddonName) { - return addonName === "auth" ? "https://proofkit.proof.sh/auth/fm-addon" : "https://proofkit.proof.sh/docs/webviewer"; -} - -function getAddonManifestUrl() { - return process.env.PROOFKIT_FM_ADDON_MANIFEST_URL || DEFAULT_FM_ADDON_MANIFEST_URL; -} - -export function resolveFmAddonDownloadDir(homeDir = os.homedir()): string { - return process.env.PROOFKIT_FM_ADDON_DOWNLOAD_DIR || path.join(homeDir, "Downloads", "ProofKit"); -} - -function parseAddonVersion(version: string) { - const parts = version - .split(".") - .map((part) => part.trim()) - .filter(Boolean); - - if (parts.length === 0 || parts.some((part) => !NUMERIC_VERSION_PART_REGEX.test(part))) { - return undefined; - } - - return parts.map((part) => Number.parseInt(part, 10)); -} - -export function compareAddonVersions(installedVersion: string, latestVersion: string) { - const installed = parseAddonVersion(installedVersion); - const latest = parseAddonVersion(latestVersion); - - if (!(installed && latest)) { - return undefined; - } - - const maxLength = Math.max(installed.length, latest.length); - for (let index = 0; index < maxLength; index += 1) { - const installedPart = installed[index] ?? 0; - const latestPart = latest[index] ?? 0; - - if (installedPart < latestPart) { - return -1; - } - if (installedPart > latestPart) { - return 1; - } - } - - return 0; -} - -async function readAddonVersionFromDirectory(addonPath: string): Promise { - const sidecarJsonPath = `${addonPath}.proofkit.json`; - if (await fs.pathExists(sidecarJsonPath)) { - const sidecarJson = (await fs.readJson(sidecarJsonPath)) as { - version?: string | number; - }; - if (typeof sidecarJson.version === "string" || typeof sidecarJson.version === "number") { - return String(sidecarJson.version); - } - } - - const templateXmlPath = path.join(addonPath, "template.xml"); - if (await fs.pathExists(templateXmlPath)) { - const templateXml = await fs.readFile(templateXmlPath, "utf8"); - const versionMatch = templateXml.match(FM_ADDON_VERSION_REGEX); - if (versionMatch?.[1]) { - return versionMatch[1]; - } - } - - const infoJsonPath = path.join(addonPath, "info.json"); - if (await fs.pathExists(infoJsonPath)) { - const infoJson = (await fs.readJson(infoJsonPath)) as { - Version?: string | number; - }; - if (typeof infoJson.Version === "string" || typeof infoJson.Version === "number") { - return String(infoJson.Version); - } - } - - return undefined; -} - -function resolveUrl(url: string, baseUrl: string) { - return new URL(url, baseUrl).toString(); -} - -function getRemoteFileName(remoteAssetUrl: string) { - try { - return path.basename(new URL(remoteAssetUrl).pathname); - } catch { - return path.basename(remoteAssetUrl); - } -} - -function pickAddonEntry(manifest: FmAddonManifest, addonName: FmAddonName): FmAddonManifestEntry | undefined { - const target = getAddonTarget(addonName); - const latestVersion = manifest.latestVersion; - - if (latestVersion && manifest.versions?.length) { - const latestEntry = manifest.versions.find((entry) => entry.version === latestVersion); - const addon = latestEntry?.addons?.[target]; - if (addon) { - return { ...addon, version: addon.version ?? latestEntry?.version }; - } - if (latestEntry?.assets?.length) { - return { version: latestEntry.version, assets: latestEntry.assets }; - } - } - - return manifest.addons?.[target]; -} - -function pickAddonAsset(entry: FmAddonManifestEntry): FmAddonManifestAsset | undefined { - const assets = entry.assets ?? (entry.url ? [{ url: entry.url, file: entry.file }] : []); - return ( - assets.find((asset) => asset.file?.toLowerCase().endsWith(".fmaddon")) ?? - assets.find((asset) => asset.url?.toLowerCase().endsWith(".fmaddon")) ?? - assets[0] - ); -} - -export async function resolveRemoteFmAddon(addonName: FmAddonName) { - const manifestUrl = getAddonManifestUrl(); - const response = await requestJson(manifestUrl); - if (response.status < 200 || response.status >= 300) { - throw new Error(`Could not fetch FileMaker add-on manifest (${response.status}).`); - } - - const entry = pickAddonEntry(response.data, addonName); - const asset = entry ? pickAddonAsset(entry) : undefined; - if (!(entry && asset?.url)) { - throw new Error(`Manifest does not include a ${getAddonDisplayName(addonName)} asset.`); - } - - return { - version: entry.version ?? entry.latestVersion ?? response.data.latestVersion, - url: resolveUrl(asset.url, manifestUrl), - file: asset.file, - sha256: asset.sha256, - }; -} - -export async function inspectFmAddon( - { - addonName, - }: { - addonName: FmAddonName; - }, - options?: { - targetDir?: string | null; - latestAddonPath?: string; - }, -): Promise { - const addonDir = getAddonDir(addonName); - const addonDisplayName = getAddonDisplayName(addonName); - const targetDir = options && "targetDir" in options ? options.targetDir : resolveFmAddonDownloadDir(); - const remoteAddon = options?.latestAddonPath - ? { - latestVersion: await readAddonVersionFromDirectory(options.latestAddonPath), - remoteAssetUrl: options.latestAddonPath, - } - : await resolveRemoteFmAddon(addonName) - .then((addon) => ({ - latestVersion: addon.version, - remoteAssetUrl: addon.url, - })) - .catch((error) => ({ - latestVersion: undefined, - remoteAssetUrl: getAddonManifestUrl(), - reason: error instanceof Error ? error.message : "remote-manifest-unavailable", - })); - - if (!targetDir) { - return { - status: "unknown", - addonName, - addonDir, - addonDisplayName, - targetDir: null, - installedPath: null, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - reason: "unsupported-platform", - }; - } - - const remoteFileName = getRemoteFileName(remoteAddon.remoteAssetUrl); - const installedCandidates = [ - remoteFileName ? path.join(targetDir, remoteFileName) : undefined, - path.join(targetDir, "ProofKit.fmaddon"), - path.join(targetDir, `${addonDir}.fmaddon`), - path.join(targetDir, addonDir), - ].filter((candidate): candidate is string => Boolean(candidate)); - const installedPath = ( - await Promise.all( - installedCandidates.map(async (candidate) => ((await fs.pathExists(candidate)) ? candidate : null)), - ) - ).find((candidate): candidate is string => Boolean(candidate)); - if (!installedPath) { - return { - status: "missing", - addonName, - addonDir, - addonDisplayName, - targetDir, - installedPath: installedCandidates[0] ?? null, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - }; - } - - const installedVersion = await readAddonVersionFromDirectory(installedPath); - if (!(installedVersion && remoteAddon.latestVersion)) { - return { - status: "unknown", - addonName, - addonDir, - addonDisplayName, - targetDir, - installedPath, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - installedVersion, - reason: installedVersion ? "remote-version-unavailable" : "unreadable-version", - }; - } - - const comparison = compareAddonVersions(installedVersion, remoteAddon.latestVersion); - if (comparison === undefined) { - return { - status: "unknown", - addonName, - addonDir, - addonDisplayName, - targetDir, - installedPath, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - installedVersion, - reason: "invalid-version", - }; - } - - return { - status: comparison < 0 ? "installed-outdated" : "installed-current", - addonName, - addonDir, - addonDisplayName, - targetDir, - installedPath, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - installedVersion, - }; -} - -export function getFmAddonInstallInstructions(addonName: FmAddonName) { - const addonDisplayName = getAddonDisplayName(addonName); - const docsUrl = getAddonDocsUrl(addonName); - return { - addonDisplayName, - docsUrl, - steps: [ - `Download the latest ${addonDisplayName} add-on from ${docsUrl}`, - "When FileMaker opens the add-on file, confirm the install prompt", - `Open your FileMaker file, go to layout mode, and add the ${addonDisplayName} add-on to the file`, - ], - }; -} - -export async function installFmAddonExplicitly({ addonName }: { addonName: FmAddonName }) { - const addonDisplayName = getAddonDisplayName(addonName); - const addonDir = getAddonDir(addonName); - - const remoteAddon = await resolveRemoteFmAddon(addonName); - const addonResponse = await requestArrayBuffer(remoteAddon.url); - if (addonResponse.status < 200 || addonResponse.status >= 300) { - throw new Error(`Could not download ${addonDisplayName} (${addonResponse.status}).`); - } - if (remoteAddon.sha256) { - const digest = crypto.createHash("sha256").update(addonResponse.data).digest("hex"); - if (digest !== remoteAddon.sha256) { - throw new Error(`Downloaded ${addonDisplayName} checksum did not match the manifest.`); - } - } - - const targetDir = resolveFmAddonDownloadDir(); - await fs.ensureDir(targetDir); - const addonPath = path.join(targetDir, remoteAddon.file || `${addonDir}.fmaddon`); - await fs.writeFile(addonPath, addonResponse.data); - await fs.writeJson( - `${addonPath}.proofkit.json`, - { - version: remoteAddon.version, - url: remoteAddon.url, - sha256: remoteAddon.sha256, - installedAt: new Date().toISOString(), - }, - { spaces: 2 }, - ); - - if (process.env.PROOFKIT_SKIP_OPEN_FM_ADDON !== "1") { - await openExternal(addonPath); - } - - console.log(""); - console.log(chalk.bgYellow(" ACTION REQUIRED: ")); - if (addonName === "auth") { - console.log( - `${chalk.yellowBright( - "The FM Auth add-on file was downloaded and opened.", - )} ${chalk.dim("(Learn more: https://proofkit.proof.sh/auth/fm-addon)")}`, - ); - } else { - console.log( - `${chalk.yellowBright( - "The ProofKit Web Viewer add-on file was downloaded and opened.", - )} ${chalk.dim("(Learn more: https://proofkit.proof.sh/docs/webviewer)")}`, - ); - } - const steps = [ - "When FileMaker opens the add-on file, confirm the install prompt", - `Open your FileMaker file, go to layout mode, and add the ${addonDisplayName} add-on to the file`, - `If FileMaker did not open automatically, open ${addonPath}`, - ]; - steps.forEach((step, index) => { - console.log(`${index + 1}. ${step}`); - }); - return true; -} - -export function installFmAddon({ addonName }: { addonName: FmAddonName }) { - return installFmAddonExplicitly({ addonName }); -} diff --git a/packages/cli/src/installers/proofkit-webviewer.ts b/packages/cli/src/installers/proofkit-webviewer.ts deleted file mode 100644 index b11d8ab9..00000000 --- a/packages/cli/src/installers/proofkit-webviewer.ts +++ /dev/null @@ -1,133 +0,0 @@ -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 { state } from "~/state.js"; -import { readSettings } from "~/utils/parseSettings.js"; -import { type FmAddonInspection, getFmAddonInstallInstructions, inspectFmAddon } from "./install-fm-addon.js"; - -export interface WebViewerAddonStatus { - hasRequiredLayouts?: boolean; - inspection: FmAddonInspection; -} - -export async function checkForWebViewerLayouts(projectDir = state.projectDir): Promise { - const settings = readSettings(projectDir); - const inspection = await inspectFmAddon({ addonName: "wv" }); - - 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 { inspection }; - } - if (settings.envFile) { - dotenv.config({ - path: path.join(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 { inspection }; - } - - const existingLayouts = await getLayouts({ - dataApiKey, - fmFile, - server, - }); - const webviewerLayouts = ["ProofKitWV"]; - - const allWebViewerLayoutsExist = webviewerLayouts.every((layout) => - existingLayouts.some((l: string) => l === layout), - ); - - return { - hasRequiredLayouts: allWebViewerLayoutsExist, - inspection, - }; -} - -export function getWebViewerAddonMessages({ hasRequiredLayouts, inspection }: WebViewerAddonStatus): { - info: string[]; - warn: string[]; - nextSteps: string[]; -} { - const messages = { - info: [] as string[], - warn: [] as string[], - nextSteps: [] as string[], - }; - - const instructions = getFmAddonInstallInstructions(inspection.addonName); - const { docsUrl } = instructions; - - if (hasRequiredLayouts) { - messages.info.push("Successfully detected all required layouts for ProofKit Web Viewer in your FileMaker file."); - } - - if (inspection.status === "installed-outdated") { - const versionSuffix = - inspection.installedVersion && inspection.latestVersion - ? ` Local version: ${inspection.installedVersion}. Latest version: ${inspection.latestVersion}.` - : ""; - messages.warn.push(`New ProofKit Web Viewer add-on available. See ${docsUrl} to update it.${versionSuffix}`); - messages.nextSteps.push(`Update the ProofKit Web Viewer add-on: ${docsUrl}`); - } - - if (inspection.status === "unknown" && inspection.reason === "unsupported-platform") { - messages.warn.push("Could not inspect the local ProofKit Web Viewer add-on on this platform."); - } - - if (hasRequiredLayouts === false) { - messages.warn.push( - "ProofKit Web Viewer layouts were not detected in your FileMaker file. The add-on may not be installed in the file yet.", - ); - if (inspection.status === "missing") { - messages.warn.push( - `Local ProofKit Web Viewer add-on file was not found. See ${docsUrl} to download and install it.`, - ); - messages.nextSteps.push(`Install the ProofKit Web Viewer add-on: ${docsUrl}`); - } - if (inspection.status === "unknown" && inspection.reason !== "unsupported-platform") { - messages.warn.push( - "Could not determine the local ProofKit Web Viewer add-on version. Reinstall it explicitly if you need the latest local files.", - ); - messages.nextSteps.push(`Install the ProofKit Web Viewer add-on: ${docsUrl}`); - } - messages.info.push( - chalk.bgYellow(" ACTION REQUIRED: ") + - ` Install or update the ProofKit Web Viewer add-on in your FileMaker file. ${chalk.dim(`(Learn more: ${docsUrl})`)}`, - ); - for (const step of instructions.steps) { - messages.info.push(step); - } - } - - return messages; -} - -export async function ensureWebViewerAddonInstalled() { - const status = await checkForWebViewerLayouts(); - const messages = getWebViewerAddonMessages(status); - - for (const message of messages.warn) { - console.log(chalk.yellow(message)); - } - for (const message of messages.info) { - console.log(message); - } - - return status; -} diff --git a/packages/cli/src/package-versions.ts b/packages/cli/src/package-versions.ts deleted file mode 100644 index ed5f3a8f..00000000 --- a/packages/cli/src/package-versions.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const CLI_VERSION: string = "2.2.2"; -export const FMDAPI_VERSION = "5.1.2" as const; -export const BETTER_AUTH_VERSION = "0.4.1" as const; -export const WEBVIEWER_VERSION = "3.1.0" as const; -export const TYPEGEN_VERSION = "1.1.3" as const; diff --git a/packages/cli/src/services/live.ts b/packages/cli/src/services/live.ts deleted file mode 100644 index e0d8621d..00000000 --- a/packages/cli/src/services/live.ts +++ /dev/null @@ -1,844 +0,0 @@ -import { randomUUID } from "node:crypto"; -import path from "node:path"; -import type { Effect as Fx } from "effect"; -import { Effect, Layer } from "effect"; -import { execa } from "execa"; -import fs from "fs-extra"; -import { TEMPLATE_ROOT } from "~/consts.js"; -import { - CliContext, - type CliContextValue, - CodegenService, - ConsoleService, - type FileMakerBootstrapArtifacts, - FileMakerService, - FileSystemService, - GitService, - type OttoApiKeyInfo, - type OttoFileInfo, - PackageManagerService, - ProcessService, - PromptService, - SettingsService, - TemplateService, -} from "~/core/context.js"; -import { ExternalCommandError, FileMakerSetupError, FileSystemError, UserCancelledError } from "~/core/errors.js"; -import type { AppType, FileMakerInputs, ProofKitSettings, UIType } from "~/core/types.js"; -import { installFmAddonExplicitly } from "~/installers/install-fm-addon.js"; -import { openBrowser } from "~/utils/browserOpen.js"; -import { deleteJson, getJson, postJson } from "~/utils/http.js"; -import { detectUserPackageManager } from "~/utils/packageManager.js"; -import { createDataSourceEnvNames, updateEnvSchemaFile, updateTypegenConfig } from "~/utils/projectFiles.js"; -import { - confirmPrompt, - spinner as createSpinner, - isCancel, - log, - multiSearchSelectPrompt, - note, - passwordPrompt, - searchSelectPrompt, - selectPrompt, - textPrompt, -} from "~/utils/prompts.js"; - -function unwrap(value: T | symbol): T { - if (isCancel(value)) { - throw new UserCancelledError({ message: "User aborted the operation" }); - } - return value as T; -} - -function normalizeUrl(serverUrl: string) { - if (serverUrl.startsWith("https://")) { - return serverUrl; - } - if (serverUrl.startsWith("http://")) { - return serverUrl.replace("http://", "https://"); - } - return `https://${serverUrl}`; -} - -interface LayoutFolder { - isFolder?: boolean; - name?: string; - folderLayoutNames?: LayoutFolder[]; -} - -function transformLayoutList(layouts: LayoutFolder[]): string[] { - const flatten = (layout: LayoutFolder): string[] => { - if (layout.isFolder === true) { - const folderLayouts = Array.isArray(layout.folderLayoutNames) ? layout.folderLayoutNames : []; - return folderLayouts.flatMap((item) => flatten(item)); - } - return typeof layout.name === "string" ? [layout.name] : []; - }; - - return layouts.flatMap(flatten).sort((left, right) => left.localeCompare(right)); -} - -function withFsError(operation: string, targetPath: string, run: () => Promise) { - return Effect.tryPromise({ - try: run, - catch: (cause) => - new FileSystemError({ - message: `File system ${operation} failed for ${targetPath}.`, - operation, - path: targetPath, - cause, - }), - }); -} - -function withCommandError(command: string, args: string[], cwd: string, run: () => Promise, message?: string) { - return Effect.tryPromise({ - try: run, - catch: (cause) => - new ExternalCommandError({ - message: message ?? `Command failed: ${[command, ...args].join(" ")}`, - command, - args, - cwd, - cause, - }), - }); -} - -function withFileMakerSetupError(message: string, run: () => Promise) { - return Effect.tryPromise({ - try: run, - catch: (cause) => - new FileMakerSetupError({ - message, - cause, - }), - }); -} - -const promptService = { - text: async (options: { message: string; defaultValue?: string; validate?: (value: string) => string | undefined }) => - unwrap( - await textPrompt({ - message: options.message, - defaultValue: options.defaultValue, - validate: options.validate, - }), - ).toString(), - password: async (options: { message: string; validate?: (value: string) => string | undefined }) => - unwrap( - await passwordPrompt({ - message: options.message, - validate: options.validate, - }), - ).toString(), - select: async (options: { - message: string; - options: Array<{ value: T; label: string; hint?: string }>; - }) => - unwrap( - await selectPrompt({ - message: options.message, - options: options.options, - }), - ) as T, - searchSelect: async (options: { - message: string; - emptyMessage?: string; - options: Array<{ - value: T; - label: string; - hint?: string; - keywords?: string[]; - disabled?: boolean | string; - }>; - }) => unwrap(await searchSelectPrompt(options)) as T, - multiSearchSelect: async (options: { - message: string; - options: Array<{ - value: T; - label: string; - hint?: string; - keywords?: string[]; - disabled?: boolean | string; - }>; - required?: boolean; - }) => unwrap(await multiSearchSelectPrompt(options)), - confirm: async (options: { message: string; initialValue?: boolean }) => - unwrap( - await confirmPrompt({ - message: options.message, - initialValue: options.initialValue, - }), - ) as boolean, -}; - -const consoleService = { - info: (message: string) => log.info(message), - warn: (message: string) => log.warn(message), - error: (message: string) => log.error(message), - success: (message: string) => log.success(message), - note: (message: string, title?: string) => note(message, title), -}; - -const fileSystemService = { - exists: (targetPath: string) => withFsError("exists", targetPath, () => fs.pathExists(targetPath)), - readdir: (targetPath: string) => withFsError("readdir", targetPath, () => fs.readdir(targetPath)), - ensureDir: (targetPath: string) => withFsError("ensureDir", targetPath, () => fs.ensureDir(targetPath)), - emptyDir: (targetPath: string) => withFsError("emptyDir", targetPath, () => fs.emptyDir(targetPath)), - copyDir: (from: string, to: string, options?: { overwrite?: boolean }) => - withFsError("copyDir", `${from} -> ${to}`, () => fs.copy(from, to, { overwrite: options?.overwrite ?? true })), - rename: (from: string, to: string) => withFsError("rename", `${from} -> ${to}`, () => fs.rename(from, to)), - remove: (targetPath: string) => withFsError("remove", targetPath, () => fs.remove(targetPath)), - readJson: (targetPath: string) => withFsError("readJson", targetPath, () => fs.readJson(targetPath) as Promise), - writeJson: (targetPath: string, value: unknown) => - withFsError("writeJson", targetPath, () => fs.writeJson(targetPath, value, { spaces: 2 })), - writeFile: (targetPath: string, content: string) => - withFsError("writeFile", targetPath, () => fs.writeFile(targetPath, content, "utf8")), - readFile: (targetPath: string) => withFsError("readFile", targetPath, () => fs.readFile(targetPath, "utf8")), -}; - -const templateService = { - getTemplateDir: (appType: AppType, _ui: UIType) => { - if (appType === "webviewer") { - return path.join(TEMPLATE_ROOT, "vite-wv"); - } - return path.join(TEMPLATE_ROOT, "nextjs-shadcn"); - }, -}; - -const packageManagerService = { - getVersion: (packageManager: string, cwd: string) => { - if (packageManager === "bun") { - return Effect.succeed(undefined); - } - return withCommandError(packageManager, ["-v"], cwd, async () => { - const { stdout } = await execa(packageManager, ["-v"], { cwd }); - return stdout.trim(); - }); - }, -}; - -const processService = { - run: ( - command: string, - args: string[], - options: { - cwd: string; - stdout?: "pipe" | "inherit" | "ignore"; - stderr?: "pipe" | "inherit" | "ignore"; - }, - ) => - withCommandError(command, args, options.cwd, async () => { - const result = await execa(command, args, { - cwd: options.cwd, - stdout: options.stdout ?? "pipe", - stderr: options.stderr ?? "pipe", - }); - return { - stdout: result.stdout ?? "", - stderr: result.stderr ?? "", - }; - }), -}; - -const gitService = { - initialize: (projectDir: string) => - Effect.gen(function* () { - yield* withCommandError("git", ["init"], projectDir, () => execa("git", ["init"], { cwd: projectDir })); - yield* withCommandError("git", ["add", "."], projectDir, () => execa("git", ["add", "."], { cwd: projectDir })); - yield* withCommandError("git", ["commit", "-m", "Initial commit"], projectDir, () => - execa("git", ["commit", "-m", "Initial commit"], { cwd: projectDir }), - ).pipe( - Effect.catchTag("ExternalCommandError", () => - Effect.sync(() => { - consoleService.warn("Git initial commit failed; continuing without commit."); - }), - ), - ); - }), -}; - -const settingsService = { - writeSettings: (projectDir: string, settings: ProofKitSettings) => - withFsError("writeSettings", path.join(projectDir, "proofkit.json"), () => - fs.writeJson(path.join(projectDir, "proofkit.json"), settings, { - spaces: 2, - }), - ), - appendEnvVars: (projectDir: string, vars: Record) => - withFsError("appendEnvVars", path.join(projectDir, ".env"), async () => { - const envPath = path.join(projectDir, ".env"); - const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, "utf8") : ""; - const additions = Object.entries(vars) - .map(([name, value]) => `${name}=${value}`) - .join("\n"); - const nextContent = [existing.trimEnd(), additions].filter(Boolean).join("\n").concat("\n"); - await fs.writeFile(envPath, nextContent, "utf8"); - }), -}; - -function createDataSourceEntry(dataSourceName: string) { - return { - type: "fm" as const, - name: dataSourceName, - envNames: createDataSourceEnvNames(dataSourceName), - }; -} - -function createFileMakerBootstrapArtifacts( - settings: ProofKitSettings, - inputs: FileMakerInputs, - appType: AppType, -): Promise { - const dataSourceEntry = createDataSourceEntry(inputs.dataSourceName); - const nextSettings: ProofKitSettings = { - ...settings, - dataSources: settings.dataSources.some((entry) => entry.name === dataSourceEntry.name) - ? settings.dataSources - : [...settings.dataSources, dataSourceEntry], - }; - - if (inputs.mode === "local-fm-mcp") { - return Promise.resolve({ - settings: nextSettings, - envVars: {}, - envSchemaEntries: [], - typegenConfig: { - mode: inputs.mode, - dataSourceName: inputs.dataSourceName, - fmMcpBaseUrl: inputs.fmMcpBaseUrl, - connectedFileName: inputs.fileName, - layoutName: inputs.layoutName, - schemaName: inputs.schemaName, - appType, - }, - }); - } - - return Promise.resolve({ - settings: nextSettings, - envVars: { - [inputs.envNames.database]: inputs.fileName, - [inputs.envNames.server]: inputs.server, - [inputs.envNames.apiKey]: inputs.dataApiKey, - }, - envSchemaEntries: [ - { - name: inputs.envNames.database, - zodSchema: 'z.string().endsWith(".fmp12")', - defaultValue: inputs.fileName, - }, - { - name: inputs.envNames.server, - zodSchema: "z.string().url()", - defaultValue: inputs.server, - }, - { - name: inputs.envNames.apiKey, - zodSchema: 'z.string().startsWith("dk_")', - defaultValue: inputs.dataApiKey, - }, - ], - typegenConfig: { - mode: inputs.mode, - dataSourceName: inputs.dataSourceName, - envNames: inputs.envNames, - layoutName: inputs.layoutName, - schemaName: inputs.schemaName, - appType, - }, - }); -} - -const fileMakerService = { - detectLocalFmMcp: (baseUrl = process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365") => - Effect.tryPromise({ - try: async () => { - try { - const health = await fetch(`${baseUrl}/health`, { - signal: AbortSignal.timeout(3000), - }); - if (!health.ok) { - return { baseUrl, healthy: false, connectedFiles: [] }; - } - const connectedFiles = await fetch(`${baseUrl}/connectedFiles`, { - signal: AbortSignal.timeout(3000), - }) - .then(async (response) => (response.ok ? ((await response.json()) as unknown) : [])) - .catch(() => []); - return { - baseUrl, - healthy: true, - connectedFiles: Array.isArray(connectedFiles) - ? connectedFiles.filter((item): item is string => typeof item === "string") - : [], - }; - } catch { - return { baseUrl, healthy: false, connectedFiles: [] }; - } - }, - catch: (cause) => - new FileMakerSetupError({ - message: "Unable to detect local ProofKit plugin.", - cause, - }), - }), - authorizeLocalFmMcp: ({ - baseUrl, - fileName, - interactive, - clientName, - clientDescription, - }: { - baseUrl: string; - fileName: string; - interactive: boolean; - clientName: string; - clientDescription: string; - }) => - Effect.tryPromise({ - try: async () => { - if (!interactive) { - throw new Error("interactive authorization disabled"); - } - const sessionToken = `pk_${randomUUID().replaceAll("-", "")}`; - const response = await postJson<{ status?: unknown; error?: unknown }>( - `${baseUrl}/authorizeSession`, - { - sessionId: sessionToken, - fileName, - clientName, - clientDescription, - }, - { timeout: 125_000 }, - ); - if (response.status >= 200 && response.status < 300 && response.data?.status === "approved") { - return { sessionToken }; - } - const status = response.data?.status; - let reason = "authorization failed"; - if (typeof response.data?.error === "string") { - reason = response.data.error; - } else if (status === "rejected") { - reason = "authorization rejected"; - } else if (status === "timeout") { - reason = "authorization timed out"; - } - throw new Error(reason); - }, - catch: (cause) => - new FileMakerSetupError({ - message: `Not authorized to connect to FileMaker file "${fileName}".`, - cause, - }), - }), - installLocalWebViewerAddon: () => - Effect.tryPromise({ - try: async () => { - await installFmAddonExplicitly({ addonName: "wv" }); - }, - catch: (cause) => - new FileMakerSetupError({ - message: "Unable to install local ProofKit Web Viewer add-on files.", - cause, - }), - }).pipe(Effect.asVoid), - validateHostedServerUrl: (serverUrl: string, ottoPort?: number | null) => - Effect.gen(function* () { - const normalizedUrl = normalizeUrl(serverUrl); - const fmsUrl = new URL("/fmws/serverinfo", normalizedUrl).toString(); - const fmsResponse = yield* withFileMakerSetupError( - `Unable to validate FileMaker Server URL: ${normalizedUrl}`, - () => getJson<{ data?: { ServerVersion?: string } }>(fmsUrl), - ); - const serverVersion = fmsResponse.data?.data?.ServerVersion?.split(" ")[0]; - if (!serverVersion) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: `Invalid FileMaker Server URL: ${normalizedUrl}`, - }), - ); - } - - let ottoVersion: string | null = null; - const otto4Response = yield* withFileMakerSetupError("Unable to query OttoFMS version.", () => - getJson<{ response?: { Otto?: { version?: string } } }>(new URL("/otto/api/info", normalizedUrl).toString()), - ).pipe(Effect.catchAll(() => Effect.succeed(undefined))); - ottoVersion = otto4Response?.data?.response?.Otto?.version ?? null; - - if (!ottoVersion) { - const otto3Url = new URL(normalizedUrl); - otto3Url.port = ottoPort ? String(ottoPort) : "3030"; - otto3Url.pathname = "/api/otto/info"; - const otto3Response = yield* withFileMakerSetupError("Unable to query OttoFMS v3 version.", () => - getJson<{ Otto?: { version?: string } }>(otto3Url.toString()), - ).pipe(Effect.catchAll(() => Effect.succeed(undefined))); - ottoVersion = otto3Response?.data?.Otto?.version ?? null; - } - - return { - normalizedUrl: new URL(normalizedUrl).origin, - versions: { - fmsVersion: serverVersion, - ottoVersion, - }, - }; - }), - getOttoFMSToken: ({ url }: { url: URL }) => - Effect.gen(function* () { - const hash = randomUUID().replaceAll("-", "").slice(0, 18); - const loginUrl = new URL(`/otto/wizard/${hash}`, url.origin); - log.info(`If the browser window didn't open automatically, use this Otto login URL:\n${loginUrl.toString()}`); - yield* withFileMakerSetupError("Unable to open OttoFMS login URL.", () => openBrowser(loginUrl.toString())); - - const spin = createSpinner(); - spin.start("Waiting for OttoFMS login"); - - const deadline = Date.now() + 180_000; - while (Date.now() < deadline) { - const response = yield* withFileMakerSetupError("Unable to poll OttoFMS login status.", () => - getJson<{ response?: { token?: string } }>(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { "Accept-Encoding": "deflate" }, - timeout: 5000, - }), - ).pipe(Effect.catchAll(() => Effect.succeed(undefined))); - const token = response?.data?.response?.token; - if (token) { - spin.stop("Login complete"); - yield* withFileMakerSetupError("Unable to clean up OttoFMS login state.", () => - deleteJson(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { "Accept-Encoding": "deflate" }, - }), - ).pipe(Effect.catchAll(() => Effect.void)); - return { token }; - } - yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 500))); - } - - spin.stop("Login timed out"); - return yield* Effect.fail( - new FileMakerSetupError({ - message: "OttoFMS login timed out after 3 minutes.", - }), - ); - }), - listFiles: ({ url, token }: { url: URL; token: string }) => - Effect.gen(function* () { - const response = yield* withFileMakerSetupError("Unable to list FileMaker files from OttoFMS.", () => - getJson<{ - response?: { - databases?: Array<{ filename?: string; status?: string }>; - }; - }>(`${url.origin}/otto/fmi/admin/api/v2/databases`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }), - ); - const databases = Array.isArray(response.data?.response?.databases) ? response.data.response.databases : []; - return databases - .filter((database): database is { filename: string; status?: string } => typeof database.filename === "string") - .map( - (database) => - ({ - filename: database.filename, - status: database.status ?? "unknown", - }) satisfies OttoFileInfo, - ); - }), - listAPIKeys: ({ url, token }: { url: URL; token: string }) => - Effect.gen(function* () { - const response = yield* withFileMakerSetupError("Unable to list OttoFMS Data API keys.", () => - getJson<{ response?: { "api-keys"?: Record[] } }>(`${url.origin}/otto/api/api-key`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }), - ); - const apiKeys = Array.isArray(response.data?.response?.["api-keys"]) ? response.data.response["api-keys"] : []; - return apiKeys - .filter( - ( - apiKey, - ): apiKey is { - key: string; - user: string; - database: string; - label: string; - } => - typeof apiKey.key === "string" && - typeof apiKey.user === "string" && - typeof apiKey.database === "string" && - typeof apiKey.label === "string", - ) - .map( - (apiKey) => - ({ - key: apiKey.key, - user: apiKey.user, - database: apiKey.database, - label: apiKey.label, - }) satisfies OttoApiKeyInfo, - ); - }), - createDataAPIKeyWithCredentials: ({ - url, - filename, - username, - password: userPassword, - }: { - url: URL; - filename: string; - username: string; - password: string; - }) => - Effect.gen(function* () { - const response = yield* withFileMakerSetupError(`Unable to create a Data API key for ${filename}.`, () => - postJson<{ response?: { key?: string } }>(`${url.origin}/otto/api/api-key/create-only`, { - database: filename, - label: "For FM Web App", - user: username, - pass: userPassword, - }), - ); - const apiKey = response.data?.response?.key; - if (!apiKey) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: `Failed to create a Data API key for ${filename}.`, - }), - ); - } - return { apiKey }; - }), - startDeployment: ({ payload, url, token }: { payload: unknown; url: URL; token: string }) => - withFileMakerSetupError("Unable to start ProofKit Demo deployment.", () => - postJson<{ response?: { subDeploymentIds?: number[] } }>(`${url.origin}/otto/api/deployment`, payload, { - headers: { - Authorization: `Bearer ${token}`, - }, - }), - ), - getDeploymentStatus: ({ url, token, deploymentId }: { url: URL; token: string; deploymentId: number }) => - withFileMakerSetupError(`Unable to fetch deployment status for ${deploymentId}.`, () => - getJson<{ response?: { status?: string; running?: boolean } }>( - `${url.origin}/otto/api/deployment/${deploymentId}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ), - ), - deployDemoFile: ({ url, token, operation }: { url: URL; token: string; operation: "install" | "replace" }) => - Effect.gen(function* () { - const demoFileName = "ProofKitDemo.fmp12"; - const spin = createSpinner(); - spin.start("Deploying ProofKit Demo file"); - - const deploymentPayload = { - scheduled: false, - label: "Install ProofKit Demo", - deployments: [ - { - name: "Install ProofKit Demo", - source: { - type: "url", - url: "https://proofkit.proof.sh/proofkit-demo/manifest.json", - }, - fileOperations: [ - { - target: { - fileName: demoFileName, - }, - operation, - source: { - fileName: demoFileName, - }, - location: { - folder: "default", - subFolder: "", - }, - }, - ], - concurrency: 1, - options: { - closeFilesAfterBuild: false, - keepFilesClosedAfterComplete: false, - transferContainerData: false, - }, - }, - ], - abortRemaining: false, - }; - - const deployment = yield* fileMakerService.startDeployment({ - payload: deploymentPayload, - url, - token, - }); - - const deploymentId = deployment.data?.response?.subDeploymentIds?.[0]; - if (!deploymentId) { - spin.stop("Demo deployment failed"); - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No deployment ID was returned when deploying the demo file.", - }), - ); - } - - const deploymentDeadline = Date.now() + 300_000; - let deploymentCompleted = false; - while (Date.now() < deploymentDeadline) { - yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 2500))); - const status = yield* fileMakerService.getDeploymentStatus({ - url, - token, - deploymentId, - }); - - if (!status.data?.response?.running) { - if (status.data?.response?.status !== "complete") { - spin.stop("Demo deployment failed"); - return yield* Effect.fail( - new FileMakerSetupError({ - message: "ProofKit Demo deployment did not complete successfully.", - }), - ); - } - deploymentCompleted = true; - break; - } - } - - if (!deploymentCompleted) { - spin.stop("Demo deployment timed out"); - return yield* Effect.fail( - new FileMakerSetupError({ - message: "ProofKit Demo deployment timed out after 5 minutes.", - }), - ); - } - - const apiKey = yield* fileMakerService.createDataAPIKeyWithCredentials({ - url, - filename: demoFileName, - username: "admin", - password: "admin", - }); - spin.stop("Demo file deployed"); - return { apiKey: apiKey.apiKey, filename: demoFileName }; - }), - listLayouts: ({ dataApiKey, fmFile, server }: { dataApiKey: string; fmFile: string; server: string }) => - Effect.gen(function* () { - const response = yield* withFileMakerSetupError(`Unable to list layouts for ${fmFile}.`, () => - getJson<{ response?: { layouts?: LayoutFolder[] } }>( - `${server}/otto/fmi/data/vLatest/databases/${encodeURIComponent(fmFile)}/layouts`, - { - headers: { - Authorization: `Bearer ${dataApiKey}`, - }, - }, - ), - ); - const layouts = Array.isArray(response.data?.response?.layouts) ? response.data.response.layouts : []; - return transformLayoutList(layouts); - }), - createFileMakerBootstrapArtifacts: (settings: ProofKitSettings, inputs: FileMakerInputs, appType: AppType) => - withFileMakerSetupError("Unable to prepare FileMaker bootstrap artifacts.", () => - createFileMakerBootstrapArtifacts(settings, inputs, appType), - ), - bootstrap: (projectDir: string, settings: ProofKitSettings, inputs: FileMakerInputs, appType: AppType) => - Effect.gen(function* () { - const artifacts = yield* fileMakerService.createFileMakerBootstrapArtifacts(settings, inputs, appType); - const projectFilesFs = { - exists: (targetPath: string) => Effect.runPromise(fileSystemService.exists(targetPath)), - readFile: (targetPath: string) => Effect.runPromise(fileSystemService.readFile(targetPath)), - writeFile: (targetPath: string, content: string) => - Effect.runPromise(fileSystemService.writeFile(targetPath, content)), - }; - if (Object.keys(artifacts.envVars).length > 0) { - yield* settingsService.appendEnvVars(projectDir, artifacts.envVars); - yield* withFileMakerSetupError("Unable to update env schema for FileMaker bootstrap.", () => - updateEnvSchemaFile(projectFilesFs, projectDir, artifacts.envSchemaEntries), - ); - } - - yield* withFileMakerSetupError("Unable to update typegen config for FileMaker bootstrap.", () => - updateTypegenConfig(projectFilesFs, projectDir, { - appType: artifacts.typegenConfig.appType, - dataSourceName: artifacts.typegenConfig.dataSourceName, - envNames: artifacts.typegenConfig.envNames, - fmMcpBaseUrl: artifacts.typegenConfig.fmMcpBaseUrl, - connectedFileName: artifacts.typegenConfig.connectedFileName, - layoutName: artifacts.typegenConfig.layoutName, - schemaName: artifacts.typegenConfig.schemaName, - }), - ); - - return artifacts.settings; - }), -}; - -const codegenService = { - runInitial: (projectDir: string, packageManager: CliContextValue["packageManager"], proofkitToken?: string) => { - let commandParts: string[]; - if (packageManager === "npm") { - commandParts = ["npm", "run", "typegen"]; - } else if (packageManager === "bun") { - commandParts = ["bun", "run", "typegen"]; - } else { - commandParts = [packageManager, "typegen"]; - } - const command = commandParts[0]; - if (!command) { - return Effect.fail( - new ExternalCommandError({ - message: "Unable to resolve the codegen command", - command: packageManager, - args: commandParts.slice(1), - cwd: projectDir, - }), - ); - } - const args = commandParts.slice(1); - return withCommandError( - command, - args, - projectDir, - async () => { - await execa(command, args, { - cwd: projectDir, - env: proofkitToken ? { FM_MCP_SESSION_ID: proofkitToken } : undefined, - }); - }, - "Initial codegen failed", - ); - }, -}; - -export function makeLiveLayer(options: { cwd: string; debug: boolean; nonInteractive: boolean }) { - const cliContext: CliContextValue = { - cwd: options.cwd, - debug: options.debug, - nonInteractive: options.nonInteractive, - packageManager: detectUserPackageManager(), - }; - - const layer = Layer.mergeAll( - Layer.succeed(CliContext, cliContext), - Layer.succeed(PromptService, promptService), - Layer.succeed(ConsoleService, consoleService), - Layer.succeed(FileSystemService, fileSystemService), - Layer.succeed(TemplateService, templateService), - Layer.succeed(PackageManagerService, packageManagerService), - Layer.succeed(ProcessService, processService), - Layer.succeed(GitService, gitService), - Layer.succeed(SettingsService, settingsService), - Layer.succeed(FileMakerService, fileMakerService), - Layer.succeed(CodegenService, codegenService), - ); - - return (effect: Fx.Effect) => Effect.provide(effect, layer); -} diff --git a/packages/cli/src/state.ts b/packages/cli/src/state.ts deleted file mode 100644 index 1130be82..00000000 --- a/packages/cli/src/state.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod/v4"; - -const schema = z - .object({ - 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("shadcn"), - 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 }; - state = mergedState; - } -} - -export function isNonInteractiveMode() { - return state.nonInteractive; -} diff --git a/packages/cli/src/utils/browserOpen.ts b/packages/cli/src/utils/browserOpen.ts deleted file mode 100644 index 5580d98b..00000000 --- a/packages/cli/src/utils/browserOpen.ts +++ /dev/null @@ -1,11 +0,0 @@ -import open from "open"; - -export async function openBrowser(url: string): Promise { - try { - await open(url); - } catch { - // Ignore open failures and let the user copy the URL manually. - } -} - -export const openExternal: (url: string) => Promise = openBrowser; diff --git a/packages/cli/src/utils/getProofKitVersion.ts b/packages/cli/src/utils/getProofKitVersion.ts deleted file mode 100644 index 29134255..00000000 --- a/packages/cli/src/utils/getProofKitVersion.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - BETTER_AUTH_VERSION, - CLI_VERSION, - FMDAPI_VERSION, - TYPEGEN_VERSION, - WEBVIEWER_VERSION, -} from "~/package-versions.js"; - -export const getVersion = () => { - return CLI_VERSION; -}; - -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; -}; - -export const getProofkitDependencyVersion = (version: string) => `^${version}`; diff --git a/packages/cli/src/utils/http.ts b/packages/cli/src/utils/http.ts deleted file mode 100644 index 87e88bb9..00000000 --- a/packages/cli/src/utils/http.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { readFile } from "node:fs/promises"; -import https from "node:https"; -import axios from "axios"; - -function createHttpsAgent() { - return new https.Agent({ - rejectUnauthorized: process.env.PROOFKIT_ALLOW_INSECURE_TLS !== "1", - }); -} - -export async function getJson(url: string, options?: { headers?: Record; timeout?: number }) { - const response = await axios.get(url, { - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeout ?? 10_000, - validateStatus: null, - }); - return response; -} - -export async function postJson( - url: string, - data: unknown, - options?: { headers?: Record; timeout?: number }, -) { - const response = await axios.post(url, data, { - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeout ?? 10_000, - validateStatus: null, - }); - return response; -} - -export async function deleteJson(url: string, options?: { headers?: Record; timeout?: number }) { - const response = await axios.delete(url, { - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeout ?? 10_000, - validateStatus: null, - }); - return response; -} - -export async function requestJson( - url: string | URL, - options?: { - method?: "GET" | "POST" | "DELETE"; - headers?: Record; - body?: Record; - timeoutMs?: number; - }, -) { - if (url.toString().startsWith("file://")) { - return { - status: 200, - data: JSON.parse(await readFile(new URL(url), "utf8")) as T, - }; - } - - const response = await axios.request({ - url: url.toString(), - method: options?.method ?? "GET", - data: options?.body, - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeoutMs ?? 10_000, - }); - return response; -} - -export async function requestText( - url: string | URL, - options?: { - method?: "GET" | "POST" | "DELETE"; - headers?: Record; - timeoutMs?: number; - }, -) { - const response = await axios.request({ - url: url.toString(), - method: options?.method ?? "GET", - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeoutMs ?? 10_000, - responseType: "text", - validateStatus: null, - }); - return { - status: response.status, - data: response.data, - }; -} - -export async function requestArrayBuffer( - url: string | URL, - options?: { - method?: "GET"; - headers?: Record; - timeoutMs?: number; - }, -) { - if (url.toString().startsWith("file://")) { - return { - status: 200, - data: await readFile(new URL(url)), - }; - } - - const response = await axios.request({ - url: url.toString(), - method: options?.method ?? "GET", - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeoutMs ?? 30_000, - responseType: "arraybuffer", - validateStatus: null, - }); - return { - status: response.status, - data: Buffer.from(response.data), - }; -} diff --git a/packages/cli/src/utils/nonInteractive.ts b/packages/cli/src/utils/nonInteractive.ts deleted file mode 100644 index a619af17..00000000 --- a/packages/cli/src/utils/nonInteractive.ts +++ /dev/null @@ -1,35 +0,0 @@ -const NON_INTERACTIVE_ENV_VARS = [ - "CI", - "GITHUB_ACTIONS", - "CODEX", - "OPENAI_CODEX", - "CLAUDE_CODE", - "JENKINS_URL", - "BUILDKITE", -] as const; - -export function detectNonInteractiveTerminal(options?: { - stdinIsTTY?: boolean; - stdoutIsTTY?: boolean; - env?: NodeJS.ProcessEnv; -}): boolean { - const env = options?.env ?? process.env; - const hasTTY = options?.stdinIsTTY === true && options?.stdoutIsTTY === true; - const hasNonInteractiveEnv = NON_INTERACTIVE_ENV_VARS.some((name) => Boolean(env[name])); - const hasDumbTerm = env.TERM === "dumb"; - return !hasTTY || hasNonInteractiveEnv || hasDumbTerm; -} - -export function resolveNonInteractiveMode(options?: { - CI?: boolean; - nonInteractive?: boolean; - stdinIsTTY?: boolean; - stdoutIsTTY?: boolean; - env?: NodeJS.ProcessEnv; -}) { - if (options?.nonInteractive === true || options?.CI === true) { - return true; - } - - return detectNonInteractiveTerminal(options); -} diff --git a/packages/cli/src/utils/packageManager.ts b/packages/cli/src/utils/packageManager.ts deleted file mode 100644 index 15402c66..00000000 --- a/packages/cli/src/utils/packageManager.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; - -export function detectUserPackageManager(): PackageManager { - 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"; - } - - return "npm"; -} diff --git a/packages/cli/src/utils/parseNameAndPath.ts b/packages/cli/src/utils/parseNameAndPath.ts deleted file mode 100644 index 0e6fae65..00000000 --- a/packages/cli/src/utils/parseNameAndPath.ts +++ /dev/null @@ -1,46 +0,0 @@ -import pathModule from "node:path"; - -import { removeTrailingSlash } from "./removeTrailingSlash.js"; - -const whitespaceRegex = /\s+/g; - -/** - * 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("/"); - const normalizedPaths = [...paths]; - const lastPathIndex = normalizedPaths.length - 1; - - let appName = (normalizedPaths.at(-1) ?? "").replace(whitespaceRegex, "-").toLowerCase(); - normalizedPaths[lastPathIndex] = appName; - - // 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).replace(whitespaceRegex, "-").toLowerCase(); - } - - // If the first part is a @, it's a scoped package - const indexOfDelimiter = normalizedPaths.findIndex((p) => p.startsWith("@")); - if (indexOfDelimiter !== -1) { - appName = normalizedPaths.slice(indexOfDelimiter).join("/"); - } - - const path = normalizedPaths.filter((p) => !p.startsWith("@")).join("/"); - - return [appName, path] as const; -}; diff --git a/packages/cli/src/utils/parseSettings.ts b/packages/cli/src/utils/parseSettings.ts deleted file mode 100644 index 4424826a..00000000 --- a/packages/cli/src/utils/parseSettings.ts +++ /dev/null @@ -1,162 +0,0 @@ -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; -function parseSettingsFile(settingsPath: string) { - // 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); - return parsed; -} - -export const getSettings = () => { - if (settings) { - return settings; - } - - const settingsPath = path.join(state.projectDir, "proofkit.json"); - const parsed = parseSettingsFile(settingsPath); - - state.appType = parsed.appType; - settings = parsed; - return parsed; -}; - -export function readSettings(projectDir = state.projectDir) { - return parseSettingsFile(path.join(projectDir, "proofkit.json")); -} - -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/src/utils/projectFiles.ts b/packages/cli/src/utils/projectFiles.ts deleted file mode 100644 index cb5fbee6..00000000 --- a/packages/cli/src/utils/projectFiles.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { readFileSync } from "node:fs"; -import path from "node:path"; -import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser/lib/esm/main.js"; -import { PKG_ROOT } from "~/consts.js"; -import type { FileMakerEnvNames } from "~/core/types.js"; -import { getVersion } from "~/utils/getProofKitVersion.js"; -import type { PackageManager } from "~/utils/packageManager.js"; - -const commonFileMakerLayoutPrefixes = ["API_", "API ", "dapi_", "dapi"]; -const TRAILING_SLASH_REGEX = /[^/]$/; -const WHITESPACE_REGEX = /\s/; -const DEFAULT_FM_MCP_BASE_URL = "http://127.0.0.1:1365"; -const textFileExtensions = new Set([ - ".ts", - ".tsx", - ".js", - ".jsx", - ".json", - ".jsonc", - ".md", - ".css", - ".scss", - ".html", - ".mjs", - ".cjs", -]); - -export function getDefaultSchemaName(layoutName: string) { - let schemaName = layoutName.replace(/[-\s]/g, "_"); - for (const prefix of commonFileMakerLayoutPrefixes) { - if (schemaName.startsWith(prefix)) { - schemaName = schemaName.replace(prefix, ""); - } - } - return schemaName; -} - -export function createDataSourceEnvNames(dataSourceName: string): FileMakerEnvNames { - if (dataSourceName === "filemaker") { - return { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }; - } - - const upperName = dataSourceName.toUpperCase(); - return { - database: `${upperName}_FM_DATABASE`, - server: `${upperName}_FM_SERVER`, - apiKey: `${upperName}_OTTO_API_KEY`, - }; -} - -export function formatPackageManagerCommand(packageManager: PackageManager, command: string) { - return ["npm", "bun"].includes(packageManager) ? `${packageManager} run ${command}` : `${packageManager} ${command}`; -} - -export function parseCommandString(command: string): string[] { - const tokens: string[] = []; - let current = ""; - let quote: "'" | '"' | undefined; - let escaping = false; - - for (const char of command) { - if (escaping) { - current += char; - escaping = false; - continue; - } - - if (char === "\\") { - escaping = true; - continue; - } - - if (quote) { - if (char === quote) { - quote = undefined; - } else { - current += char; - } - continue; - } - - if (char === "'" || char === '"') { - quote = char; - continue; - } - - if (WHITESPACE_REGEX.test(char)) { - if (current) { - tokens.push(current); - current = ""; - } - continue; - } - - current += char; - } - - if (escaping) { - current += "\\"; - } - - if (current) { - tokens.push(current); - } - - return tokens; -} - -export function getTemplatePackageCommand(packageManager: PackageManager) { - if (packageManager === "npm") { - return "npm run"; - } - return packageManager; -} - -export function getTemplatePackageExecuteCommand(packageManager: PackageManager) { - if (packageManager === "npm") { - return "npx"; - } - if (packageManager === "pnpm") { - return "pnpx"; - } - if (packageManager === "bun") { - return "bunx"; - } - return `${packageManager} dlx`; -} - -export function normalizeImportAlias(importAlias: string) { - return importAlias.replace(/\*/g, "").replace(TRAILING_SLASH_REGEX, "$&/"); -} - -export async function replaceTextInFiles( - fs: { - readdir: (path: string) => Promise; - readFile: (path: string) => Promise; - writeFile: (path: string, content: string) => Promise; - }, - rootDir: string, - searchValue: string, - replaceValue: string, -) { - const entries = await fs.readdir(rootDir); - for (const entry of entries) { - const fullPath = path.join(rootDir, entry); - const childEntries = await fs.readdir(fullPath).catch((error: unknown) => { - const code = - typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" - ? error.code - : undefined; - - if (code === "ENOTDIR") { - return undefined; - } - - throw error; - }); - if (childEntries) { - await replaceTextInFiles(fs, fullPath, searchValue, replaceValue); - continue; - } - - const extension = path.extname(entry); - if (!textFileExtensions.has(extension)) { - continue; - } - - const content = await fs.readFile(fullPath).catch(() => undefined); - if (!content?.includes(searchValue)) { - continue; - } - - await fs.writeFile(fullPath, content.replaceAll(searchValue, replaceValue)); - } -} - -export async function updateEnvSchemaFile( - fs: { - exists: (path: string) => Promise; - readFile: (path: string) => Promise; - writeFile: (path: string, content: string) => Promise; - }, - projectDir: string, - envEntries: Array<{ name: string; zodSchema: string }>, -) { - const envFilePath = path.join(projectDir, "src/lib/env.ts"); - if (!(await fs.exists(envFilePath))) { - return; - } - - let content = await fs.readFile(envFilePath); - const marker = " server: {"; - const markerIndex = content.indexOf(marker); - if (markerIndex === -1) { - return; - } - - const insertIndex = content.indexOf(" },", markerIndex); - if (insertIndex === -1) { - return; - } - - const additions = envEntries - .filter((entry) => !content.includes(`${entry.name}:`)) - .map((entry) => ` ${entry.name}: ${entry.zodSchema},`) - .join("\n"); - - if (!additions) { - return; - } - - content = `${content.slice(0, insertIndex)}${additions}\n${content.slice(insertIndex)}`; - await fs.writeFile(envFilePath, content); -} - -interface TypegenFileContent { - $schema?: string; - config: Record[] | Record; -} - -export async function updateTypegenConfig( - fs: { - exists: (path: string) => Promise; - readFile: (path: string) => Promise; - writeFile: (path: string, content: string) => Promise; - }, - projectDir: string, - options: { - appType: "browser" | "webviewer"; - dataSourceName: string; - envNames?: FileMakerEnvNames; - fmMcpBaseUrl?: string; - connectedFileName?: string; - layoutName?: string; - schemaName?: string; - }, -) { - const configPath = path.join(projectDir, "proofkit-typegen.config.jsonc"); - const dsPath = `./src/config/schemas/${options.dataSourceName}`; - const nextDataSource: Record = { - type: "fmdapi", - layouts: [], - path: dsPath, - clearOldFiles: true, - clientSuffix: "Layout", - }; - - if (options.envNames) { - nextDataSource.envNames = { - server: options.envNames.server, - db: options.envNames.database, - auth: { apiKey: options.envNames.apiKey }, - }; - } - - if (options.appType === "webviewer") { - nextDataSource.webviewerScriptName = "ExecuteDataApi"; - } - - if (options.fmMcpBaseUrl || options.connectedFileName) { - nextDataSource.fmMcp = { - enabled: true, - ...(options.fmMcpBaseUrl && options.fmMcpBaseUrl !== DEFAULT_FM_MCP_BASE_URL - ? { baseUrl: options.fmMcpBaseUrl } - : {}), - ...(options.connectedFileName ? { connectedFileName: options.connectedFileName } : {}), - }; - } - - const layout = - options.layoutName && options.schemaName - ? { - layoutName: options.layoutName, - schemaName: options.schemaName, - valueLists: "allowEmpty", - } - : undefined; - - if (layout) { - nextDataSource.layouts = [layout]; - } - - if (!(await fs.exists(configPath))) { - const nextContent: TypegenFileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [nextDataSource], - }; - await fs.writeFile(configPath, `${JSON.stringify(nextContent, null, 2)}\n`); - return; - } - - const original = await fs.readFile(configPath); - const parsed = parseJsonc(original) as TypegenFileContent; - const configArray = Array.isArray(parsed.config) ? parsed.config : [parsed.config]; - const existingIndex = configArray.findIndex((entry) => entry.path === dsPath); - - if (existingIndex === -1) { - configArray.push(nextDataSource); - } else { - const existing = (configArray[existingIndex] ?? {}) as Record; - const existingLayouts = Array.isArray(existing.layouts) ? existing.layouts : []; - let nextLayouts = existingLayouts; - if (layout && !existingLayouts.some((item) => item?.layoutName === layout.layoutName)) { - nextLayouts = [...existingLayouts, layout]; - } - configArray[existingIndex] = { - ...existing, - ...nextDataSource, - layouts: nextLayouts, - }; - } - - const nextConfig = Array.isArray(parsed.config) ? configArray : (configArray[0] ?? nextDataSource); - const edits = modify(original, ["config"], nextConfig, { - formattingOptions: { - insertSpaces: true, - tabSize: 2, - eol: "\n", - }, - }); - await fs.writeFile(configPath, applyEdits(original, edits)); -} - -export function getScaffoldVersion() { - const generatedVersion = getVersion(); - if (generatedVersion && generatedVersion !== "0.0.0-private") { - return generatedVersion; - } - - const candidates = [path.resolve(PKG_ROOT, "package.json"), path.resolve(PKG_ROOT, "../cli/package.json")]; - - for (const candidate of candidates) { - try { - const packageJson = JSON.parse(readFileSync(candidate, "utf8")) as { - version?: string; - }; - if (packageJson.version && packageJson.version !== "0.0.0-private") { - return packageJson.version; - } - } catch { - // ignore and continue searching - } - } - - return "0.0.0-private"; -} diff --git a/packages/cli/src/utils/projectName.ts b/packages/cli/src/utils/projectName.ts deleted file mode 100644 index 2c4ee329..00000000 --- a/packages/cli/src/utils/projectName.ts +++ /dev/null @@ -1,63 +0,0 @@ -import path from "node:path"; - -const TRAILING_SLASHES_REGEX = /\/+$/; -const PATH_SEPARATOR_REGEX = /\\/g; -const WHITESPACE_REGEX = /\s+/g; -const VALID_APP_NAME_REGEX = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; - -function normalizeProjectName(value: string) { - return value.replace(PATH_SEPARATOR_REGEX, "/"); -} - -function trimTrailingSlashes(value: string) { - return normalizeProjectName(value).replace(TRAILING_SLASHES_REGEX, ""); -} - -function normalizeProjectNameForPackage(value: string) { - return trimTrailingSlashes(value).replace(WHITESPACE_REGEX, "-").toLowerCase(); -} - -export function parseNameAndPath(projectName: string): [scopedAppName: string, appDir: string] { - const normalizedProjectName = trimTrailingSlashes(projectName); - const segments = normalizedProjectName.split("/"); - const hasScopedPackage = (segments.at(-2) ?? "").startsWith("@"); - const packageSegmentCount = hasScopedPackage ? 2 : 1; - const leadingSegments = segments.slice(0, -packageSegmentCount); - const packageSegments = segments.slice(-packageSegmentCount); - const normalizedPackageSegments = packageSegments.map(normalizeProjectNameForPackage); - let scopedAppName = normalizedPackageSegments.join("/"); - let appDirPackageSegments = normalizedPackageSegments; - - if (scopedAppName === ".") { - scopedAppName = normalizeProjectNameForPackage(path.basename(path.resolve(process.cwd()))); - appDirPackageSegments = packageSegments; - } - - const appDir = [...leadingSegments, ...appDirPackageSegments].join("/"); - - return [scopedAppName, appDir]; -} - -export function validateAppName(projectName: string) { - const normalized = normalizeProjectNameForPackage(projectName); - if (normalized === ".") { - const currentDirName = path.basename(path.resolve(process.cwd())); - return VALID_APP_NAME_REGEX.test(currentDirName.replace(WHITESPACE_REGEX, "-").toLowerCase()) - ? undefined - : "Name must consist of only lowercase alphanumeric characters, '-', and '_'"; - } - - const segments = normalized.split("/"); - const scopeIndex = segments.findIndex((segment) => segment.startsWith("@")); - let scopedAppName = segments.at(-1); - - if (scopeIndex !== -1) { - scopedAppName = segments.slice(scopeIndex).join("/"); - } - - if (VALID_APP_NAME_REGEX.test(scopedAppName ?? "")) { - return; - } - - return "Name must consist of only lowercase alphanumeric characters, '-', and '_'"; -} diff --git a/packages/cli/src/utils/prompts.ts b/packages/cli/src/utils/prompts.ts deleted file mode 100644 index 5f977b7c..00000000 --- a/packages/cli/src/utils/prompts.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { - intro as clackIntro, - isCancel as clackIsCancel, - log as clackLog, - note as clackNote, - outro as clackOutro, - spinner as clackSpinner, -} 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/new/prompt-cancelled"); - -export const intro = clackIntro; -export const log = clackLog; -export const note = clackNote; -export const outro = clackOutro; -export const spinner = clackSpinner; - -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 || clackIsCancel(value); -} - -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 matchesSearch(option: SearchPromptOption, query: string) { - const haystack = [option.label, option.hint ?? "", ...(option.keywords ?? [])].join(" ").toLowerCase(); - return haystack.includes(query.trim().toLowerCase()); -} - -function normalizeDisabledMessage(value: boolean | string | undefined) { - if (typeof value === "string") { - return value; - } - return value ? true : undefined; -} - -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 textPrompt(options: { - message: string; - defaultValue?: string; - validate?: (value: string) => string | undefined; -}) { - return withCancelSentinel(() => - inquirerInput({ - message: options.message, - default: options.defaultValue, - validate: normalizeValidate(options.validate), - }), - ); -} - -export function passwordPrompt(options: { message: string; validate?: (value: string) => string | undefined }) { - return withCancelSentinel(() => - inquirerPassword({ - message: options.message, - validate: normalizeValidate(options.validate), - }), - ); -} - -export function confirmPrompt(options: { message: string; initialValue?: boolean }) { - return withCancelSentinel(() => - inquirerConfirm({ - message: options.message, - default: options.initialValue, - }), - ); -} - -export function selectPrompt(options: { message: string; options: PromptOption[] }) { - return withCancelSentinel(() => - inquirerSelect({ - message: options.message, - pageSize: 10, - choices: options.options.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })), - }), - ); -} - -export function searchSelectPrompt(options: { - message: 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 multiSearchSelectPrompt(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/src/utils/removeTrailingSlash.ts b/packages/cli/src/utils/removeTrailingSlash.ts deleted file mode 100644 index 051c3322..00000000 --- a/packages/cli/src/utils/removeTrailingSlash.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const removeTrailingSlash = (input: string) => { - if (input.length > 1 && input.endsWith("/")) { - return input.slice(0, -1); - } - return input; -}; diff --git a/packages/cli/src/utils/renderTitle.ts b/packages/cli/src/utils/renderTitle.ts deleted file mode 100644 index 1a6a8898..00000000 --- a/packages/cli/src/utils/renderTitle.ts +++ /dev/null @@ -1,18 +0,0 @@ -import gradient from "gradient-string"; -import { getTitleText } from "~/consts.js"; -import { detectUserPackageManager } from "~/utils/packageManager.js"; - -const proofTheme = { - purple: "#89216B", - lightPurple: "#D15ABB", - orange: "#FF595E", -}; - -export const proofGradient = gradient(Object.values(proofTheme)); -export function renderTitle(version = "0.0.0-private") { - const pkgManager = detectUserPackageManager(); - if (pkgManager === "yarn" || pkgManager === "pnpm") { - console.log(""); - } - console.log(proofGradient.multiline(getTitleText(version))); -} diff --git a/packages/cli/src/utils/sortPackageJson.ts b/packages/cli/src/utils/sortPackageJson.ts deleted file mode 100644 index f2f45980..00000000 --- a/packages/cli/src/utils/sortPackageJson.ts +++ /dev/null @@ -1,86 +0,0 @@ -const ROOT_KEY_ORDER = [ - "name", - "version", - "private", - "description", - "keywords", - "homepage", - "bugs", - "repository", - "license", - "author", - "contributors", - "funding", - "type", - "packageManager", - "devEngines", - "engines", - "bin", - "exports", - "main", - "module", - "types", - "files", - "scripts", - "dependencies", - "devDependencies", - "peerDependencies", - "peerDependenciesMeta", - "optionalDependencies", - "bundledDependencies", - "resolutions", - "overrides", - "pnpm", - "lint-staged", -] as const; - -const ROOT_KEY_POSITION = new Map(ROOT_KEY_ORDER.map((key, index) => [key, index])); -const SORTED_OBJECT_KEYS = new Set([ - "scripts", - "dependencies", - "devDependencies", - "peerDependencies", - "peerDependenciesMeta", - "optionalDependencies", - "bundledDependencies", - "resolutions", - "overrides", -]); - -function compareRootKeys(left: string, right: string) { - const leftIndex = ROOT_KEY_POSITION.get(left); - const rightIndex = ROOT_KEY_POSITION.get(right); - - if (leftIndex !== undefined && rightIndex !== undefined) { - return leftIndex - rightIndex; - } - - if (leftIndex !== undefined) { - return -1; - } - - if (rightIndex !== undefined) { - return 1; - } - - return left.localeCompare(right); -} - -function sortRecord(value: Record) { - return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right))); -} - -export function sortPackageJson>(packageJson: T): T { - const sortedEntries = Object.entries(packageJson).sort(([left], [right]) => compareRootKeys(left, right)); - const sortedPackageJson = Object.fromEntries( - sortedEntries.map(([key, value]) => { - if (SORTED_OBJECT_KEYS.has(key) && value && typeof value === "object" && !Array.isArray(value)) { - return [key, sortRecord(value as Record)]; - } - - return [key, value]; - }), - ); - - return sortedPackageJson as T; -} diff --git a/packages/cli/src/utils/validateAppName.ts b/packages/cli/src/utils/validateAppName.ts deleted file mode 100644 index e2173d94..00000000 --- a/packages/cli/src/utils/validateAppName.ts +++ /dev/null @@ -1,29 +0,0 @@ -import path from "node:path"; - -import { removeTrailingSlash } from "./removeTrailingSlash.js"; - -const validationRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; -const whitespaceRegex = /\s+/g; - -//Validate a string against allowed package.json names -export const validateAppName = (rawInput: string) => { - const input = removeTrailingSlash(rawInput).replace(whitespaceRegex, "-").toLowerCase(); - 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 === ".") { - appName = path.basename(path.resolve(process.cwd())).replace(whitespaceRegex, "-").toLowerCase(); - } - - if (validationRegExp.test(appName ?? "")) { - return; - } - return "Name must consist of only lowercase alphanumeric characters, '-', and '_'"; -}; diff --git a/packages/cli/src/utils/versioning.ts b/packages/cli/src/utils/versioning.ts deleted file mode 100644 index 55aab322..00000000 --- a/packages/cli/src/utils/versioning.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getNodeMajorVersion() { - return process.versions.node.split(".")[0] ?? "22"; -} diff --git a/packages/cli/template/extras/_cursor/conditional-rules/nextjs-framework.mdc b/packages/cli/template/extras/_cursor/conditional-rules/nextjs-framework.mdc deleted file mode 100644 index 5ce7a9e0..00000000 --- a/packages/cli/template/extras/_cursor/conditional-rules/nextjs-framework.mdc +++ /dev/null @@ -1,51 +0,0 @@ ---- -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/template/extras/_cursor/conditional-rules/npm.mdc b/packages/cli/template/extras/_cursor/conditional-rules/npm.mdc deleted file mode 100644 index 3b030fa5..00000000 --- a/packages/cli/template/extras/_cursor/conditional-rules/npm.mdc +++ /dev/null @@ -1,60 +0,0 @@ ---- -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/template/extras/_cursor/conditional-rules/pnpm.mdc b/packages/cli/template/extras/_cursor/conditional-rules/pnpm.mdc deleted file mode 100644 index d25da047..00000000 --- a/packages/cli/template/extras/_cursor/conditional-rules/pnpm.mdc +++ /dev/null @@ -1,65 +0,0 @@ ---- -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/template/extras/_cursor/conditional-rules/yarn.mdc b/packages/cli/template/extras/_cursor/conditional-rules/yarn.mdc deleted file mode 100644 index 5672e80e..00000000 --- a/packages/cli/template/extras/_cursor/conditional-rules/yarn.mdc +++ /dev/null @@ -1,60 +0,0 @@ ---- -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/template/extras/_cursor/rules/cursor-rules.mdc b/packages/cli/template/extras/_cursor/rules/cursor-rules.mdc deleted file mode 100644 index 061da499..00000000 --- a/packages/cli/template/extras/_cursor/rules/cursor-rules.mdc +++ /dev/null @@ -1,88 +0,0 @@ ---- -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/template/extras/_cursor/rules/filemaker-api.mdc b/packages/cli/template/extras/_cursor/rules/filemaker-api.mdc deleted file mode 100644 index dd6d6716..00000000 --- a/packages/cli/template/extras/_cursor/rules/filemaker-api.mdc +++ /dev/null @@ -1,176 +0,0 @@ ---- -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/template/extras/_cursor/rules/troubleshooting-patterns.mdc b/packages/cli/template/extras/_cursor/rules/troubleshooting-patterns.mdc deleted file mode 100644 index 797fd3cc..00000000 --- a/packages/cli/template/extras/_cursor/rules/troubleshooting-patterns.mdc +++ /dev/null @@ -1,240 +0,0 @@ ---- -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/template/extras/_cursor/rules/ui-components.mdc b/packages/cli/template/extras/_cursor/rules/ui-components.mdc deleted file mode 100644 index 78ec63ad..00000000 --- a/packages/cli/template/extras/_cursor/rules/ui-components.mdc +++ /dev/null @@ -1,57 +0,0 @@ ---- -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/template/extras/config/drizzle-config-mysql.ts b/packages/cli/template/extras/config/drizzle-config-mysql.ts deleted file mode 100644 index 63ba29ab..00000000 --- a/packages/cli/template/extras/config/drizzle-config-mysql.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/template/extras/config/drizzle-config-postgres.ts b/packages/cli/template/extras/config/drizzle-config-postgres.ts deleted file mode 100644 index 063cc964..00000000 --- a/packages/cli/template/extras/config/drizzle-config-postgres.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/template/extras/config/drizzle-config-sqlite.ts b/packages/cli/template/extras/config/drizzle-config-sqlite.ts deleted file mode 100644 index b55a91e0..00000000 --- a/packages/cli/template/extras/config/drizzle-config-sqlite.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/template/extras/config/fmschema.config.mjs b/packages/cli/template/extras/config/fmschema.config.mjs deleted file mode 100644 index 4b940151..00000000 --- a/packages/cli/template/extras/config/fmschema.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @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/template/extras/config/get-query-client.ts b/packages/cli/template/extras/config/get-query-client.ts deleted file mode 100644 index 44598cba..00000000 --- a/packages/cli/template/extras/config/get-query-client.ts +++ /dev/null @@ -1,6 +0,0 @@ -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/template/extras/config/postcss.config.cjs b/packages/cli/template/extras/config/postcss.config.cjs deleted file mode 100644 index 51ec8fa6..00000000 --- a/packages/cli/template/extras/config/postcss.config.cjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -module.exports = config; diff --git a/packages/cli/template/extras/config/query-provider-vite.tsx b/packages/cli/template/extras/config/query-provider-vite.tsx deleted file mode 100644 index 7b1d7e2b..00000000 --- a/packages/cli/template/extras/config/query-provider-vite.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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/template/extras/config/query-provider.tsx b/packages/cli/template/extras/config/query-provider.tsx deleted file mode 100644 index 2a928906..00000000 --- a/packages/cli/template/extras/config/query-provider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"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/template/extras/emailProviders/none/email.tsx b/packages/cli/template/extras/emailProviders/none/email.tsx deleted file mode 100644 index 7cf42f10..00000000 --- a/packages/cli/template/extras/emailProviders/none/email.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { render } from "@react-email/render"; -import { AuthCodeEmail } from "@/emails/auth-code"; - -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.proof.sh/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/template/extras/emailProviders/plunk/email.tsx b/packages/cli/template/extras/emailProviders/plunk/email.tsx deleted file mode 100644 index d1e8acc7..00000000 --- a/packages/cli/template/extras/emailProviders/plunk/email.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { render } from "@react-email/render"; -import { AuthCodeEmail } from "@/emails/auth-code"; - -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/template/extras/emailProviders/plunk/service.ts b/packages/cli/template/extras/emailProviders/plunk/service.ts deleted file mode 100644 index 080ae050..00000000 --- a/packages/cli/template/extras/emailProviders/plunk/service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Plunk from "@plunk/node"; -import { env } from "@/config/env"; - -export const plunk = new Plunk(env.PLUNK_API_KEY); diff --git a/packages/cli/template/extras/emailProviders/resend/email.tsx b/packages/cli/template/extras/emailProviders/resend/email.tsx deleted file mode 100644 index 0ff4601a..00000000 --- a/packages/cli/template/extras/emailProviders/resend/email.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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/template/extras/emailProviders/resend/service.ts b/packages/cli/template/extras/emailProviders/resend/service.ts deleted file mode 100644 index 6adeac4f..00000000 --- a/packages/cli/template/extras/emailProviders/resend/service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Resend } from "resend"; -import { env } from "@/config/env"; - -export const resend = new Resend(env.RESEND_API_KEY); diff --git a/packages/cli/template/extras/emailTemplates/auth-code.tsx b/packages/cli/template/extras/emailTemplates/auth-code.tsx deleted file mode 100644 index 1e7191f2..00000000 --- a/packages/cli/template/extras/emailTemplates/auth-code.tsx +++ /dev/null @@ -1,155 +0,0 @@ -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) => ( - - - - - ProofKit - - {type === "verification" - ? "Verify Your Email" - : "Reset Your Password"} - - - Enter the following code to{" "} - {type === "verification" - ? "verify your email" - : "reset your password"} - -
- {validationCode} -
- - 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/template/extras/emailTemplates/generic.tsx b/packages/cli/template/extras/emailTemplates/generic.tsx deleted file mode 100644 index 33e4ef49..00000000 --- a/packages/cli/template/extras/emailTemplates/generic.tsx +++ /dev/null @@ -1,135 +0,0 @@ -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) => ( - - - - - ProofKit - - {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.proof.sh", - 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/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts deleted file mode 100644 index 586f9c74..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts +++ /dev/null @@ -1,97 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -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 { 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/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx deleted file mode 100644 index f6dfe126..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Anchor, Container, Paper, Stack, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { getCurrentSession } from "@/server/auth/utils/session"; - -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/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx deleted file mode 100644 index 9fe14bf7..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"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 ( -
- - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - {form.formState.isDirty && ( - - - - )} - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx deleted file mode 100644 index 896a4d25..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"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 { useState } from "react"; -import { showSuccessNotification } from "@/utils/notification-helpers"; - -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 ( -
- - - - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts deleted file mode 100644 index 3984756a..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts deleted file mode 100644 index bdd2d779..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts +++ /dev/null @@ -1,39 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -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 { 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/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx deleted file mode 100644 index 1b74f9ba..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"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 ( -
- - - - - {action.result.data?.error && ( - {action.result.data.error} - )} - - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx deleted file mode 100644 index a3c2bc7a..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts deleted file mode 100644 index 618832e7..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const forgotPasswordSchema = z.object({ - email: z.string().email(), -}); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/login/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/login/actions.ts deleted file mode 100644 index 06e4a939..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/login/actions.ts +++ /dev/null @@ -1,35 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -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 { 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/template/extras/fmaddon-auth/app/auth/login/login-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/login/login-form.tsx deleted file mode 100644 index 41e3c938..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/login/login-form.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"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 ( -
- - - - - - - - Forgot password? - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/login/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/login/page.tsx deleted file mode 100644 index 0f66ba7a..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/login/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { getCurrentSession } from "@/server/auth/utils/session"; - -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/template/extras/fmaddon-auth/app/auth/login/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/login/schema.ts deleted file mode 100644 index 95feeb80..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/login/schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { z } from "zod/v4"; - -export const loginSchema = z.object({ - email: z.string().email(), - password: z.string(), -}); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts deleted file mode 100644 index 7e7a46d2..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts +++ /dev/null @@ -1,53 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -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 { 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/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx deleted file mode 100644 index 6c25efe5..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Alert, Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { env } from "@/config/env"; -import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset"; - -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/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx deleted file mode 100644 index 82d70c6c..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"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 ( -
- - - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts deleted file mode 100644 index 3973a81f..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -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/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts deleted file mode 100644 index 26ca4924..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts +++ /dev/null @@ -1,46 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { - setPasswordResetSessionAsEmailVerified, - validatePasswordResetSessionRequest, -} from "@/server/auth/utils/password-reset"; -import { setUserAsEmailVerifiedIfEmailMatches } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; - -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 (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/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx deleted file mode 100644 index af23cee9..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Alert, Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { env } from "@/config/env"; -import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset"; - -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/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts deleted file mode 100644 index efd19a95..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const verifyEmailSchema = z.object({ - code: z.string().length(8), -}); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx deleted file mode 100644 index 07516c86..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"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 ( -
- - - { - form.setValue("code", value); - if (value.length === 8) { - handleSubmitWithAction(); - } - }} - error={!!form.formState.errors.code?.message} - autoFocus - /> - {form.formState.errors.code?.message && ( - {form.formState.errors.code.message} - )} - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/signup/actions.ts deleted file mode 100644 index 40d77b3d..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/actions.ts +++ /dev/null @@ -1,50 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -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 { 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/template/extras/fmaddon-auth/app/auth/signup/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/signup/page.tsx deleted file mode 100644 index b9614dc7..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { getCurrentSession } from "@/server/auth/utils/session"; - -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/template/extras/fmaddon-auth/app/auth/signup/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/signup/schema.ts deleted file mode 100644 index 61ac2e86..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx deleted file mode 100644 index 6b2868a4..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"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 ( -
- - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts deleted file mode 100644 index 3c79df22..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts +++ /dev/null @@ -1,109 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -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 { 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/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx deleted file mode 100644 index f6cded7e..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"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 ( -
- - - { - form.setValue("code", value); - if (value.length === 8) { - handleSubmitWithAction(); - } - }} - /> - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx deleted file mode 100644 index 13e831ab..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { getUserEmailVerificationRequestFromRequest } from "@/server/auth/utils/email-verification"; -import { getRedirectCookie } from "@/server/auth/utils/redirect"; -import { getCurrentSession } from "@/server/auth/utils/session"; - -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/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx deleted file mode 100644 index 44ecc0af..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"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/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts deleted file mode 100644 index 323bf7eb..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const emailVerificationSchema = z.object({ - code: z.string().length(8), -}); diff --git a/packages/cli/template/extras/fmaddon-auth/components/auth/actions.ts b/packages/cli/template/extras/fmaddon-auth/components/auth/actions.ts deleted file mode 100644 index f19dd156..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/actions.ts +++ /dev/null @@ -1,19 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { - getCurrentSession, - invalidateSession, -} from "@/server/auth/utils/session"; - -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/template/extras/fmaddon-auth/components/auth/protect.tsx b/packages/cli/template/extras/fmaddon-auth/components/auth/protect.tsx deleted file mode 100644 index 666efe74..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/protect.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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/template/extras/fmaddon-auth/components/auth/redirect.tsx b/packages/cli/template/extras/fmaddon-auth/components/auth/redirect.tsx deleted file mode 100644 index 8e3bb965..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/redirect.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"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/template/extras/fmaddon-auth/components/auth/use-user.ts b/packages/cli/template/extras/fmaddon-auth/components/auth/use-user.ts deleted file mode 100644 index 02d829cd..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/use-user.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import type { Session } from "@/server/auth/utils/session"; -import type { User } from "@/server/auth/utils/user"; - -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/template/extras/fmaddon-auth/components/auth/user-menu.tsx b/packages/cli/template/extras/fmaddon-auth/components/auth/user-menu.tsx deleted file mode 100644 index 06c99b7c..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/user-menu.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"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 ( - - - - - - } - > - My Profile - - - } - onClick={logout} - > - Sign out - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/emails/auth-code.tsx b/packages/cli/template/extras/fmaddon-auth/emails/auth-code.tsx deleted file mode 100644 index 1e7191f2..00000000 --- a/packages/cli/template/extras/fmaddon-auth/emails/auth-code.tsx +++ /dev/null @@ -1,155 +0,0 @@ -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) => ( - - - - - ProofKit - - {type === "verification" - ? "Verify Your Email" - : "Reset Your Password"} - - - Enter the following code to{" "} - {type === "verification" - ? "verify your email" - : "reset your password"} - -
- {validationCode} -
- - 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/template/extras/fmaddon-auth/middleware.ts b/packages/cli/template/extras/fmaddon-auth/middleware.ts deleted file mode 100644 index b544bd32..00000000 --- a/packages/cli/template/extras/fmaddon-auth/middleware.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } 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/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts deleted file mode 100644 index f04799d0..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { encodeBase32 } from "@oslojs/encoding"; -import { cookies } from "next/headers"; - -import { emailVerificationLayout } from "../db/client"; -import type { 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/template/extras/fmaddon-auth/server/auth/utils/encryption.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/encryption.ts deleted file mode 100644 index 2ba22737..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/encryption.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { DynamicBuffer } from "@oslojs/binary"; -import { decodeBase64 } from "@oslojs/encoding"; -import { createCipheriv, createDecipheriv } from "crypto"; - -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/template/extras/fmaddon-auth/server/auth/utils/index.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/index.ts deleted file mode 100644 index 13f64ce3..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -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/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts deleted file mode 100644 index 38f7bf3d..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { sha256 } from "@oslojs/crypto/sha2"; -import { encodeHexLowerCase } from "@oslojs/encoding"; -import { cookies } from "next/headers"; - -import { passwordResetLayout } from "../db/client"; -import type { 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/template/extras/fmaddon-auth/server/auth/utils/password.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password.ts deleted file mode 100644 index e637db4d..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { hash, verify } from "@node-rs/argon2"; -import { sha1 } from "@oslojs/crypto/sha1"; -import { encodeHexLowerCase } from "@oslojs/encoding"; -import { options } from "."; - -/** - * 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/template/extras/fmaddon-auth/server/auth/utils/redirect.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/redirect.ts deleted file mode 100644 index 4655871e..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/redirect.ts +++ /dev/null @@ -1,8 +0,0 @@ -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/template/extras/fmaddon-auth/server/auth/utils/session.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/session.ts deleted file mode 100644 index 7b69d061..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/session.ts +++ /dev/null @@ -1,191 +0,0 @@ -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/template/extras/fmaddon-auth/server/auth/utils/user.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/user.ts deleted file mode 100644 index 3ed77da2..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/user.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { usersLayout } from "../db/client"; -import type { 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/template/extras/prisma/schema/base-planetscale.prisma b/packages/cli/template/extras/prisma/schema/base-planetscale.prisma deleted file mode 100644 index 6b9dd139..00000000 --- a/packages/cli/template/extras/prisma/schema/base-planetscale.prisma +++ /dev/null @@ -1,24 +0,0 @@ -// 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/template/extras/prisma/schema/base.prisma b/packages/cli/template/extras/prisma/schema/base.prisma deleted file mode 100644 index ddb6e099..00000000 --- a/packages/cli/template/extras/prisma/schema/base.prisma +++ /dev/null @@ -1,20 +0,0 @@ -// 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/template/extras/prisma/schema/with-auth-planetscale.prisma b/packages/cli/template/extras/prisma/schema/with-auth-planetscale.prisma deleted file mode 100644 index 198915b9..00000000 --- a/packages/cli/template/extras/prisma/schema/with-auth-planetscale.prisma +++ /dev/null @@ -1,77 +0,0 @@ -// 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/template/extras/prisma/schema/with-auth.prisma b/packages/cli/template/extras/prisma/schema/with-auth.prisma deleted file mode 100644 index b17831e6..00000000 --- a/packages/cli/template/extras/prisma/schema/with-auth.prisma +++ /dev/null @@ -1,74 +0,0 @@ -// 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/template/extras/src/app/_components/post-tw.tsx b/packages/cli/template/extras/src/app/_components/post-tw.tsx deleted file mode 100644 index fabff895..00000000 --- a/packages/cli/template/extras/src/app/_components/post-tw.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"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.

- )} -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className="flex flex-col gap-2" - > - setName(e.target.value)} - className="w-full rounded-full px-4 py-2 text-black" - /> - -
-
- ); -} diff --git a/packages/cli/template/extras/src/app/_components/post.tsx b/packages/cli/template/extras/src/app/_components/post.tsx deleted file mode 100644 index 56235b0c..00000000 --- a/packages/cli/template/extras/src/app/_components/post.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"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.

- )} - -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className={styles.form} - > - setName(e.target.value)} - className={styles.input} - /> - -
-
- ); -} diff --git a/packages/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts b/packages/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 6112167f..00000000 --- a/packages/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { handlers } from "@/server/auth"; // Referring to the auth.ts we just created - -export const { GET, POST } = handlers; diff --git a/packages/cli/template/extras/src/app/api/trpc/[trpc]/route.ts b/packages/cli/template/extras/src/app/api/trpc/[trpc]/route.ts deleted file mode 100644 index 0658508b..00000000 --- a/packages/cli/template/extras/src/app/api/trpc/[trpc]/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -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/template/extras/src/app/clerk-auth/layout.tsx b/packages/cli/template/extras/src/app/clerk-auth/layout.tsx deleted file mode 100644 index e74f0b09..00000000 --- a/packages/cli/template/extras/src/app/clerk-auth/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Center } from "@mantine/core"; -import type React from "react"; - -export default function AuthLayout({ - children, -}: { - children: React.ReactNode; -}) { - return
{children}
; -} diff --git a/packages/cli/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx b/packages/cli/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx deleted file mode 100644 index 405f94c3..00000000 --- a/packages/cli/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SignIn } from "@clerk/nextjs"; - -export default function Page() { - return ; -} diff --git a/packages/cli/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx b/packages/cli/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx deleted file mode 100644 index 4b651293..00000000 --- a/packages/cli/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SignUp } from "@clerk/nextjs"; - -export default function Page() { - return ; -} diff --git a/packages/cli/template/extras/src/app/layout/base.tsx b/packages/cli/template/extras/src/app/layout/base.tsx deleted file mode 100644 index b1748540..00000000 --- a/packages/cli/template/extras/src/app/layout/base.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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/template/extras/src/app/layout/main-shell.tsx b/packages/cli/template/extras/src/app/layout/main-shell.tsx deleted file mode 100644 index f561571a..00000000 --- a/packages/cli/template/extras/src/app/layout/main-shell.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { - AppShell, - AppShellFooter, - AppShellHeader, - AppShellMain, - AppShellNavbar, -} from "@mantine/core"; -import type 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/template/extras/src/app/layout/with-trpc-tw.tsx b/packages/cli/template/extras/src/app/layout/with-trpc-tw.tsx deleted file mode 100644 index 9c0b2a5d..00000000 --- a/packages/cli/template/extras/src/app/layout/with-trpc-tw.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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/template/extras/src/app/layout/with-trpc.tsx b/packages/cli/template/extras/src/app/layout/with-trpc.tsx deleted file mode 100644 index 54285b9d..00000000 --- a/packages/cli/template/extras/src/app/layout/with-trpc.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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/template/extras/src/app/layout/with-tw.tsx b/packages/cli/template/extras/src/app/layout/with-tw.tsx deleted file mode 100644 index fdc321da..00000000 --- a/packages/cli/template/extras/src/app/layout/with-tw.tsx +++ /dev/null @@ -1,20 +0,0 @@ -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/template/extras/src/app/next-auth/layout.tsx b/packages/cli/template/extras/src/app/next-auth/layout.tsx deleted file mode 100644 index f87b5175..00000000 --- a/packages/cli/template/extras/src/app/next-auth/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Card, Center } from "@mantine/core"; -import { redirect } from "next/navigation"; -import type React from "react"; -import { auth } from "@/server/auth"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const session = await auth(); - if (session) { - return redirect("/"); - } - return ( -
- - {children} - -
- ); -} diff --git a/packages/cli/template/extras/src/app/next-auth/signin/page.tsx b/packages/cli/template/extras/src/app/next-auth/signin/page.tsx deleted file mode 100644 index 82ef5eff..00000000 --- a/packages/cli/template/extras/src/app/next-auth/signin/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { - Button, - Card, - Divider, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import Link from "next/link"; -import { redirect } from "next/navigation"; -import { AuthError } from "next-auth"; -import { providerMap, signIn } from "@/server/auth"; - -export default async function SignInPage(props: { - searchParams: Promise<{ callbackUrl: string | undefined }>; -}) { - const searchParams = await props.searchParams; - return ( - -
{ - "use server"; - try { - await signIn("credentials", formData); - } catch (error) { - if (error instanceof AuthError) { - return redirect(`/auth/signin?error=${error.type}`); - } - throw error; - } - }} - > - - - - - - -
- {providerMap.length > 0 && ( - <> - - {Object.values(providerMap).map((provider) => ( -
{ - "use server"; - try { - await signIn(provider.id, { - redirectTo: searchParams.callbackUrl ?? "", - }); - } catch (error) { - // Signin can fail for a number of reasons, such as the user - // not existing, or the user not having the correct role. - // In some cases, you may want to redirect to a custom error - if (error instanceof AuthError) { - return redirect(`/auth/signin?error=${error.type}`); - } - - // Otherwise if a redirects happens Next.js can handle it - // so you can just re-thrown the error and let Next.js handle it. - // Docs: - // https://nextjs.org/docs/app/api-reference/functions/redirect#server-component - throw error; - } - }} - > - -
- ))} - - )} - - - {"Don't have an account? "} - Sign up - -
- ); -} diff --git a/packages/cli/template/extras/src/app/next-auth/signup/action.ts b/packages/cli/template/extras/src/app/next-auth/signup/action.ts deleted file mode 100644 index ad7ce394..00000000 --- a/packages/cli/template/extras/src/app/next-auth/signup/action.ts +++ /dev/null @@ -1,24 +0,0 @@ -"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/template/extras/src/app/next-auth/signup/page.tsx b/packages/cli/template/extras/src/app/next-auth/signup/page.tsx deleted file mode 100644 index f87909c5..00000000 --- a/packages/cli/template/extras/src/app/next-auth/signup/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"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/template/extras/src/app/next-auth/signup/validation.ts b/packages/cli/template/extras/src/app/next-auth/signup/validation.ts deleted file mode 100644 index 58ef8d09..00000000 --- a/packages/cli/template/extras/src/app/next-auth/signup/validation.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/template/extras/src/app/page/base.tsx b/packages/cli/template/extras/src/app/page/base.tsx deleted file mode 100644 index b40d0174..00000000 --- a/packages/cli/template/extras/src/app/page/base.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Text } from "@mantine/core"; -import Link from "next/link"; - -export default function Home() { - return Welcome!; -} diff --git a/packages/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx b/packages/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx deleted file mode 100644 index c0c08909..00000000 --- a/packages/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx +++ /dev/null @@ -1,67 +0,0 @@ -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/template/extras/src/app/page/with-auth-trpc.tsx b/packages/cli/template/extras/src/app/page/with-auth-trpc.tsx deleted file mode 100644 index ba555b87..00000000 --- a/packages/cli/template/extras/src/app/page/with-auth-trpc.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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/template/extras/src/app/page/with-trpc-tw.tsx b/packages/cli/template/extras/src/app/page/with-trpc-tw.tsx deleted file mode 100644 index ac985714..00000000 --- a/packages/cli/template/extras/src/app/page/with-trpc-tw.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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/template/extras/src/app/page/with-trpc.tsx b/packages/cli/template/extras/src/app/page/with-trpc.tsx deleted file mode 100644 index 17993fde..00000000 --- a/packages/cli/template/extras/src/app/page/with-trpc.tsx +++ /dev/null @@ -1,54 +0,0 @@ -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/template/extras/src/app/page/with-tw.tsx b/packages/cli/template/extras/src/app/page/with-tw.tsx deleted file mode 100644 index 8fbf44a6..00000000 --- a/packages/cli/template/extras/src/app/page/with-tw.tsx +++ /dev/null @@ -1,37 +0,0 @@ -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/template/extras/src/components/clerk-auth/clerk-provider.tsx b/packages/cli/template/extras/src/components/clerk-auth/clerk-provider.tsx deleted file mode 100644 index b8c89f54..00000000 --- a/packages/cli/template/extras/src/components/clerk-auth/clerk-provider.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"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/template/extras/src/components/clerk-auth/user-menu-mobile.tsx b/packages/cli/template/extras/src/components/clerk-auth/user-menu-mobile.tsx deleted file mode 100644 index d60fde9b..00000000 --- a/packages/cli/template/extras/src/components/clerk-auth/user-menu-mobile.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"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/template/extras/src/components/clerk-auth/user-menu.tsx b/packages/cli/template/extras/src/components/clerk-auth/user-menu.tsx deleted file mode 100644 index 6a942c89..00000000 --- a/packages/cli/template/extras/src/components/clerk-auth/user-menu.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { UserButton, useClerk, 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/template/extras/src/components/next-auth/next-auth-provider.tsx b/packages/cli/template/extras/src/components/next-auth/next-auth-provider.tsx deleted file mode 100644 index 1a91cb83..00000000 --- a/packages/cli/template/extras/src/components/next-auth/next-auth-provider.tsx +++ /dev/null @@ -1,14 +0,0 @@ -"use client"; - -import type { 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/template/extras/src/components/next-auth/user-menu-mobile.tsx b/packages/cli/template/extras/src/components/next-auth/user-menu-mobile.tsx deleted file mode 100644 index 3caf59e5..00000000 --- a/packages/cli/template/extras/src/components/next-auth/user-menu-mobile.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"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/template/extras/src/components/next-auth/user-menu.tsx b/packages/cli/template/extras/src/components/next-auth/user-menu.tsx deleted file mode 100644 index 017cc7f3..00000000 --- a/packages/cli/template/extras/src/components/next-auth/user-menu.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"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 ( - - - - - - signOut()}>Sign Out - - - ); - - return null; -} diff --git a/packages/cli/template/extras/src/env/with-auth.ts b/packages/cli/template/extras/src/env/with-auth.ts deleted file mode 100644 index e111bbe0..00000000 --- a/packages/cli/template/extras/src/env/with-auth.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/template/extras/src/env/with-clerk.ts b/packages/cli/template/extras/src/env/with-clerk.ts deleted file mode 100644 index 63a04790..00000000 --- a/packages/cli/template/extras/src/env/with-clerk.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/template/extras/src/index.module.css b/packages/cli/template/extras/src/index.module.css deleted file mode 100644 index ba0f6cbd..00000000 --- a/packages/cli/template/extras/src/index.module.css +++ /dev/null @@ -1,177 +0,0 @@ -.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/template/extras/src/middleware/clerk.ts b/packages/cli/template/extras/src/middleware/clerk.ts deleted file mode 100644 index dd4b23ac..00000000 --- a/packages/cli/template/extras/src/middleware/clerk.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/template/extras/src/middleware/next-auth.ts b/packages/cli/template/extras/src/middleware/next-auth.ts deleted file mode 100644 index 7d1da17b..00000000 --- a/packages/cli/template/extras/src/middleware/next-auth.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { auth as middleware } from "@/server/auth"; - -export const config = { - matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], -}; diff --git a/packages/cli/template/extras/src/pages/_app/base.tsx b/packages/cli/template/extras/src/pages/_app/base.tsx deleted file mode 100644 index 3a94afdf..00000000 --- a/packages/cli/template/extras/src/pages/_app/base.tsx +++ /dev/null @@ -1,14 +0,0 @@ -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/template/extras/src/pages/_app/with-auth-trpc-tw.tsx b/packages/cli/template/extras/src/pages/_app/with-auth-trpc-tw.tsx deleted file mode 100644 index 52967262..00000000 --- a/packages/cli/template/extras/src/pages/_app/with-auth-trpc-tw.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { GeistSans } from "geist/font/sans"; -import type { AppType } from "next/app"; -import type { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; - -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/template/extras/src/pages/_app/with-auth-trpc.tsx b/packages/cli/template/extras/src/pages/_app/with-auth-trpc.tsx deleted file mode 100644 index 52967262..00000000 --- a/packages/cli/template/extras/src/pages/_app/with-auth-trpc.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { GeistSans } from "geist/font/sans"; -import type { AppType } from "next/app"; -import type { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; - -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/template/extras/src/pages/_app/with-auth-tw.tsx b/packages/cli/template/extras/src/pages/_app/with-auth-tw.tsx deleted file mode 100644 index 86d44cc0..00000000 --- a/packages/cli/template/extras/src/pages/_app/with-auth-tw.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { GeistSans } from "geist/font/sans"; -import type { AppType } from "next/app"; -import type { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; - -import "~/styles/globals.css"; - -const MyApp: AppType<{ session: Session | null }> = ({ - Component, - pageProps: { session, ...pageProps }, -}) => { - return ( - -
- -
-
- ); -}; - -export default MyApp; diff --git a/packages/cli/template/extras/src/pages/_app/with-auth.tsx b/packages/cli/template/extras/src/pages/_app/with-auth.tsx deleted file mode 100644 index 86d44cc0..00000000 --- a/packages/cli/template/extras/src/pages/_app/with-auth.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { GeistSans } from "geist/font/sans"; -import type { AppType } from "next/app"; -import type { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; - -import "~/styles/globals.css"; - -const MyApp: AppType<{ session: Session | null }> = ({ - Component, - pageProps: { session, ...pageProps }, -}) => { - return ( - -
- -
-
- ); -}; - -export default MyApp; diff --git a/packages/cli/template/extras/src/pages/_app/with-trpc-tw.tsx b/packages/cli/template/extras/src/pages/_app/with-trpc-tw.tsx deleted file mode 100644 index 6ab634d4..00000000 --- a/packages/cli/template/extras/src/pages/_app/with-trpc-tw.tsx +++ /dev/null @@ -1,16 +0,0 @@ -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/template/extras/src/pages/_app/with-trpc.tsx b/packages/cli/template/extras/src/pages/_app/with-trpc.tsx deleted file mode 100644 index 6ab634d4..00000000 --- a/packages/cli/template/extras/src/pages/_app/with-trpc.tsx +++ /dev/null @@ -1,16 +0,0 @@ -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/template/extras/src/pages/_app/with-tw.tsx b/packages/cli/template/extras/src/pages/_app/with-tw.tsx deleted file mode 100644 index 4a8008df..00000000 --- a/packages/cli/template/extras/src/pages/_app/with-tw.tsx +++ /dev/null @@ -1,14 +0,0 @@ -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/template/extras/src/pages/api/auth/[...nextauth].ts b/packages/cli/template/extras/src/pages/api/auth/[...nextauth].ts deleted file mode 100644 index 8739530f..00000000 --- a/packages/cli/template/extras/src/pages/api/auth/[...nextauth].ts +++ /dev/null @@ -1,5 +0,0 @@ -import NextAuth from "next-auth"; - -import { authOptions } from "~/server/auth"; - -export default NextAuth(authOptions); diff --git a/packages/cli/template/extras/src/pages/api/trpc/[trpc].ts b/packages/cli/template/extras/src/pages/api/trpc/[trpc].ts deleted file mode 100644 index f7b95612..00000000 --- a/packages/cli/template/extras/src/pages/api/trpc/[trpc].ts +++ /dev/null @@ -1,19 +0,0 @@ -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/template/extras/src/pages/index/base.tsx b/packages/cli/template/extras/src/pages/index/base.tsx deleted file mode 100644 index a91be744..00000000 --- a/packages/cli/template/extras/src/pages/index/base.tsx +++ /dev/null @@ -1,47 +0,0 @@ -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/template/extras/src/pages/index/with-auth-trpc-tw.tsx b/packages/cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx deleted file mode 100644 index 5dc6cbbd..00000000 --- a/packages/cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import Head from "next/head"; -import Link from "next/link"; -import { signIn, signOut, useSession } from "next-auth/react"; - -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/template/extras/src/pages/index/with-auth-trpc.tsx b/packages/cli/template/extras/src/pages/index/with-auth-trpc.tsx deleted file mode 100644 index d87845f7..00000000 --- a/packages/cli/template/extras/src/pages/index/with-auth-trpc.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import Head from "next/head"; -import Link from "next/link"; -import { signIn, signOut, useSession } from "next-auth/react"; - -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/template/extras/src/pages/index/with-trpc-tw.tsx b/packages/cli/template/extras/src/pages/index/with-trpc-tw.tsx deleted file mode 100644 index 6e4f5b78..00000000 --- a/packages/cli/template/extras/src/pages/index/with-trpc-tw.tsx +++ /dev/null @@ -1,52 +0,0 @@ -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/template/extras/src/pages/index/with-trpc.tsx b/packages/cli/template/extras/src/pages/index/with-trpc.tsx deleted file mode 100644 index d546b756..00000000 --- a/packages/cli/template/extras/src/pages/index/with-trpc.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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/template/extras/src/pages/index/with-tw.tsx b/packages/cli/template/extras/src/pages/index/with-tw.tsx deleted file mode 100644 index 0e2348e4..00000000 --- a/packages/cli/template/extras/src/pages/index/with-tw.tsx +++ /dev/null @@ -1,45 +0,0 @@ -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/template/extras/src/server/api/root.ts b/packages/cli/template/extras/src/server/api/root.ts deleted file mode 100644 index 374285c1..00000000 --- a/packages/cli/template/extras/src/server/api/root.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/template/extras/src/server/api/routers/post/base.ts b/packages/cli/template/extras/src/server/api/routers/post/base.ts deleted file mode 100644 index 81684a39..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/base.ts +++ /dev/null @@ -1,40 +0,0 @@ -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/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/packages/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts deleted file mode 100644 index 25d652c9..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts +++ /dev/null @@ -1,39 +0,0 @@ -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/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/packages/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts deleted file mode 100644 index af01f88e..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts +++ /dev/null @@ -1,41 +0,0 @@ -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/template/extras/src/server/api/routers/post/with-auth.ts b/packages/cli/template/extras/src/server/api/routers/post/with-auth.ts deleted file mode 100644 index 55083180..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-auth.ts +++ /dev/null @@ -1,37 +0,0 @@ -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/template/extras/src/server/api/routers/post/with-drizzle.ts b/packages/cli/template/extras/src/server/api/routers/post/with-drizzle.ts deleted file mode 100644 index 1c9bb7b7..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-drizzle.ts +++ /dev/null @@ -1,30 +0,0 @@ -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/template/extras/src/server/api/routers/post/with-prisma.ts b/packages/cli/template/extras/src/server/api/routers/post/with-prisma.ts deleted file mode 100644 index ad083cb1..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-prisma.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/template/extras/src/server/api/trpc-app/base.ts b/packages/cli/template/extras/src/server/api/trpc-app/base.ts deleted file mode 100644 index 9f8814e8..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-app/base.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * 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/template/extras/src/server/api/trpc-app/with-auth-db.ts b/packages/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts deleted file mode 100644 index 08d10c6b..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * 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/template/extras/src/server/api/trpc-app/with-auth.ts b/packages/cli/template/extras/src/server/api/trpc-app/with-auth.ts deleted file mode 100644 index 3510a908..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-app/with-auth.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * 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/template/extras/src/server/api/trpc-app/with-db.ts b/packages/cli/template/extras/src/server/api/trpc-app/with-db.ts deleted file mode 100644 index b8349dc0..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-app/with-db.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * 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/template/extras/src/server/api/trpc-pages/base.ts b/packages/cli/template/extras/src/server/api/trpc-pages/base.ts deleted file mode 100644 index 3fb05c12..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-pages/base.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * 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/template/extras/src/server/api/trpc-pages/with-auth-db.ts b/packages/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts deleted file mode 100644 index f7dc0e5d..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * 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/template/extras/src/server/api/trpc-pages/with-auth.ts b/packages/cli/template/extras/src/server/api/trpc-pages/with-auth.ts deleted file mode 100644 index 4d795bb3..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-pages/with-auth.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * 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/template/extras/src/server/api/trpc-pages/with-db.ts b/packages/cli/template/extras/src/server/api/trpc-pages/with-db.ts deleted file mode 100644 index cf0be485..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-pages/with-db.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * 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/template/extras/src/server/data/users.ts b/packages/cli/template/extras/src/server/data/users.ts deleted file mode 100644 index c5074992..00000000 --- a/packages/cli/template/extras/src/server/data/users.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/template/extras/src/server/db/db-prisma-planetscale.ts b/packages/cli/template/extras/src/server/db/db-prisma-planetscale.ts deleted file mode 100644 index fc54a43a..00000000 --- a/packages/cli/template/extras/src/server/db/db-prisma-planetscale.ts +++ /dev/null @@ -1,22 +0,0 @@ -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/template/extras/src/server/db/db-prisma.ts b/packages/cli/template/extras/src/server/db/db-prisma.ts deleted file mode 100644 index 8b2507b7..00000000 --- a/packages/cli/template/extras/src/server/db/db-prisma.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/template/extras/src/server/db/index-drizzle/with-mysql.ts b/packages/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts deleted file mode 100644 index 807acea9..00000000 --- a/packages/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/template/extras/src/server/db/index-drizzle/with-planetscale.ts b/packages/cli/template/extras/src/server/db/index-drizzle/with-planetscale.ts deleted file mode 100644 index 4613a4c1..00000000 --- a/packages/cli/template/extras/src/server/db/index-drizzle/with-planetscale.ts +++ /dev/null @@ -1,7 +0,0 @@ -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/template/extras/src/server/db/index-drizzle/with-postgres.ts b/packages/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts deleted file mode 100644 index df18baff..00000000 --- a/packages/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/template/extras/src/server/db/index-drizzle/with-sqlite.ts b/packages/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts deleted file mode 100644 index 274f7413..00000000 --- a/packages/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type Client, createClient } 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/template/extras/src/server/db/schema-drizzle/base-mysql.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts deleted file mode 100644 index 79b03dc3..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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/template/extras/src/server/db/schema-drizzle/base-planetscale.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts deleted file mode 100644 index 79b03dc3..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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/template/extras/src/server/db/schema-drizzle/base-postgres.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts deleted file mode 100644 index 6433f25d..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 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/template/extras/src/server/db/schema-drizzle/base-sqlite.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts deleted file mode 100644 index 792fe71f..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts deleted file mode 100644 index 63781a66..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts +++ /dev/null @@ -1,123 +0,0 @@ -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/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts deleted file mode 100644 index 601e5e38..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts +++ /dev/null @@ -1,117 +0,0 @@ -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/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts deleted file mode 100644 index 810e13ab..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts +++ /dev/null @@ -1,130 +0,0 @@ -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/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts deleted file mode 100644 index 3af6c96f..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts +++ /dev/null @@ -1,116 +0,0 @@ -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/template/extras/src/server/next-auth/base.ts b/packages/cli/template/extras/src/server/next-auth/base.ts deleted file mode 100644 index 2f8cde96..00000000 --- a/packages/cli/template/extras/src/server/next-auth/base.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { OttoAdapter } from "@proofkit/fmdapi"; -import NextAuth, { type DefaultSession } from "next-auth"; -import type { Provider } from "next-auth/providers"; -import Credentials from "next-auth/providers/credentials"; -import { FilemakerAdapter } from "next-auth-adapter-filemaker"; -import { z } from "zod/v4"; -import { env } from "@/config/env"; - -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/template/extras/src/server/next-auth/password.ts b/packages/cli/template/extras/src/server/next-auth/password.ts deleted file mode 100644 index d0cd6a95..00000000 --- a/packages/cli/template/extras/src/server/next-auth/password.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/template/extras/src/server/next-auth/with-drizzle.ts b/packages/cli/template/extras/src/server/next-auth/with-drizzle.ts deleted file mode 100644 index 63879d2e..00000000 --- a/packages/cli/template/extras/src/server/next-auth/with-drizzle.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import { - type DefaultSession, - getServerSession, - 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/template/extras/src/server/next-auth/with-prisma.ts b/packages/cli/template/extras/src/server/next-auth/with-prisma.ts deleted file mode 100644 index 2fb4c0ec..00000000 --- a/packages/cli/template/extras/src/server/next-auth/with-prisma.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PrismaAdapter } from "@auth/prisma-adapter"; -import { - type DefaultSession, - getServerSession, - 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/template/extras/src/trpc/query-client.ts b/packages/cli/template/extras/src/trpc/query-client.ts deleted file mode 100644 index b89a1794..00000000 --- a/packages/cli/template/extras/src/trpc/query-client.ts +++ /dev/null @@ -1,25 +0,0 @@ -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/template/extras/src/trpc/react.tsx b/packages/cli/template/extras/src/trpc/react.tsx deleted file mode 100644 index ea02db57..00000000 --- a/packages/cli/template/extras/src/trpc/react.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { type QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; -import { createTRPCReact } from "@trpc/react-query"; -import type { inferRouterInputs, 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; -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/template/extras/src/trpc/server.ts b/packages/cli/template/extras/src/trpc/server.ts deleted file mode 100644 index 47acbe61..00000000 --- a/packages/cli/template/extras/src/trpc/server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import "server-only"; - -import { createHydrationHelpers } from "@trpc/react-query/rsc"; -import { headers } from "next/headers"; -import { cache } from "react"; - -import { type AppRouter, createCaller } 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/template/extras/src/utils/api.ts b/packages/cli/template/extras/src/utils/api.ts deleted file mode 100644 index fc4f2398..00000000 --- a/packages/cli/template/extras/src/utils/api.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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, 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/template/extras/start-database/mysql.sh b/packages/cli/template/extras/start-database/mysql.sh deleted file mode 100755 index 268df5cc..00000000 --- a/packages/cli/template/extras/start-database/mysql.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/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/template/extras/start-database/postgres.sh b/packages/cli/template/extras/start-database/postgres.sh deleted file mode 100755 index 11fb2042..00000000 --- a/packages/cli/template/extras/start-database/postgres.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/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/template/nextjs-shadcn/.claude/CLAUDE.md b/packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md deleted file mode 100644 index 869eaac0..00000000 --- a/packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md +++ /dev/null @@ -1,327 +0,0 @@ -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite format` - Format and fix code automatically -- `npx ultracite lint` - Check for issues without fixing - -## Example: Error Handling -```typescript -// ✅ Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// ❌ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc b/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc deleted file mode 100644 index 98495535..00000000 --- a/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc +++ /dev/null @@ -1,333 +0,0 @@ ---- -description: Ultracite Rules - AI-Ready Formatter and Linter -globs: "**/*.{ts,tsx,js,jsx}" -alwaysApply: true ---- - -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite format` - Format and fix code automatically -- `npx ultracite lint` - Check for issues without fixing - -## Example: Error Handling -```typescript -// ✅ Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// ❌ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/.vscode/settings.json b/packages/cli/template/nextjs-shadcn/.vscode/settings.json deleted file mode 100644 index 4175042a..00000000 --- a/packages/cli/template/nextjs-shadcn/.vscode/settings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[css]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[graphql]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "typescript.tsdk": "node_modules/typescript/lib", - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "emmet.showExpandedAbbreviation": "never", - "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit", - "source.organizeImports.biome": "explicit" - } -} diff --git a/packages/cli/template/nextjs-shadcn/AGENTS.md b/packages/cli/template/nextjs-shadcn/AGENTS.md deleted file mode 100644 index 6b2924ba..00000000 --- a/packages/cli/template/nextjs-shadcn/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -__AGENT_INSTRUCTIONS__ diff --git a/packages/cli/template/nextjs-shadcn/CLAUDE.md b/packages/cli/template/nextjs-shadcn/CLAUDE.md deleted file mode 100644 index 43c994c2..00000000 --- a/packages/cli/template/nextjs-shadcn/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/packages/cli/template/nextjs-shadcn/README.md b/packages/cli/template/nextjs-shadcn/README.md deleted file mode 100644 index 80f5a13c..00000000 --- a/packages/cli/template/nextjs-shadcn/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# ProofKit NextJS Template - -This is a [NextJS](https://nextjs.org/) project bootstrapped with `@proofkit/cli`. Learn more at [proofkit.proof.sh](https://proofkit.proof.sh) - -## What's next? - -While this template is designed to be a minimal starting point, the ProofKit documentation will guide you through building your app. - -For more information, see the full [ProofKit documentation](https://proofkit.proof.sh). - -## Project Structure - -ProofKit projects have an opinionated structure to help you get started and some conventions must be maintained to ensure that the CLI can properly inject new features and components. - -The `src` directory is the home for your application code. It is used for most things except for configuration and is organized as follows: - -- `app` - NextJS app router, where your pages and routes are defined -- `components` - Shared components used throughout the app -- `server` - Code that connects to backend databases and services that should not be exposed in the browser - -Anytime you see an `internal` folder, you should not modify any files inside. These files are maintained exclusively by the ProofKit CLI and changes to them may be overwritten. - -Anytime you see a componet file that begins with `slot-`, you _may_ modify the content, but do not rename, remove, or move them. These are desigend to be customized, but are still used by the CLI to inject additional content. If a slot is not needed by your app, you can have the compoment return `null` or an empty fragment: `<>` diff --git a/packages/cli/template/nextjs-shadcn/_gitignore b/packages/cli/template/nextjs-shadcn/_gitignore deleted file mode 100644 index 1fdb37dc..00000000 --- a/packages/cli/template/nextjs-shadcn/_gitignore +++ /dev/null @@ -1,38 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -.pnpm-store -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/packages/cli/template/nextjs-shadcn/components.json b/packages/cli/template/nextjs-shadcn/components.json deleted file mode 100644 index 5bcedb31..00000000 --- a/packages/cli/template/nextjs-shadcn/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/packages/cli/template/nextjs-shadcn/next.config.ts b/packages/cli/template/nextjs-shadcn/next.config.ts deleted file mode 100644 index 543e5a3c..00000000 --- a/packages/cli/template/nextjs-shadcn/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NextConfig } from "next"; -import "@/lib/env"; - -const nextConfig: NextConfig = { - /* config options here */ -}; - -export default nextConfig; diff --git a/packages/cli/template/nextjs-shadcn/package.json b/packages/cli/template/nextjs-shadcn/package.json deleted file mode 100644 index ac00d978..00000000 --- a/packages/cli/template/nextjs-shadcn/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "raw-next", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build --turbopack", - "start": "next start", - "typegen": "typegen", - "typegen:ui": "typegen ui", - "check": "ultracite check", - "fix": "ultracite fix", - "lint": "ultracite check .", - "format": "ultracite fix ." - }, - "dependencies": { - "@radix-ui/react-slot": "^1.2.3", - "@t3-oss/env-nextjs": "^0.13.8", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.541.0", - "next": "15.5.8", - "next-themes": "^0.4.6", - "radix-ui": "^1.4.2", - "react": "19.1.1", - "react-dom": "19.1.1", - "sonner": "^2.0.4", - "tailwind-merge": "^3.3.1" - }, - "devDependencies": { - "@proofkit/typegen": "^1.1.1", - "@tailwindcss/postcss": "^4", - "@types/node": "^22", - "@types/react": "^19", - "@types/react-dom": "^19", - "oxlint": "^1.39.0", - "tailwindcss": "^4", - "tw-animate-css": "^1.3.7", - "typescript": "^5", - "ultracite": "^7.0.0" - } -} diff --git a/packages/cli/template/nextjs-shadcn/postcss.config.mjs b/packages/cli/template/nextjs-shadcn/postcss.config.mjs deleted file mode 100644 index f50127cd..00000000 --- a/packages/cli/template/nextjs-shadcn/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -const config = { - plugins: ["@tailwindcss/postcss"], -}; - -export default config; diff --git a/packages/cli/template/nextjs-shadcn/proofkit.json b/packages/cli/template/nextjs-shadcn/proofkit.json deleted file mode 100644 index 69c00095..00000000 --- a/packages/cli/template/nextjs-shadcn/proofkit.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ui": "shadcn", - "envFile": ".env", - "appType": "browser", - "registryTemplates": ["utils/t3-env"] -} diff --git a/packages/cli/template/nextjs-shadcn/public/favicon.ico b/packages/cli/template/nextjs-shadcn/public/favicon.ico deleted file mode 100644 index ba9355b8d3f888ad3a93c0f254b6776a0c2aed92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 DtPcMKfsh_-A)LFM)6iMLm_0IRoy0b^RZ#+}< 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 b29HP@D9EFJDBd}ZfIn(VT8gjb%^v(OKI6J9 diff --git a/packages/cli/template/nextjs-shadcn/src/app/(main)/layout.tsx b/packages/cli/template/nextjs-shadcn/src/app/(main)/layout.tsx deleted file mode 100644 index d9f86ecb..00000000 --- a/packages/cli/template/nextjs-shadcn/src/app/(main)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import type React from "react"; -import AppShell from "@/components/AppShell/internal/AppShell"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx b/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx deleted file mode 100644 index 979764ca..00000000 --- a/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { - ExternalLinkIcon, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; - -function GitHubMark({ size = 20 }: { size?: number }) { - return ( - - ); -} - -export default function Home() { - return ( -
-
-
- ProofKit -

Welcome!

- -

- This is the base template home page. Use the ProofKit documentation - as the primary reference while building your project. -

- -

- To change this page, open src/app/(main)/page.tsx -

- -
-
-
-
-
- Sponsored by{" "} - - Proof - {" "} - and{" "} - - Ottomatic - -
-
- - - -
-
-
-
- ); -} diff --git a/packages/cli/template/nextjs-shadcn/src/app/globals.css b/packages/cli/template/nextjs-shadcn/src/app/globals.css deleted file mode 100644 index 139104f2..00000000 --- a/packages/cli/template/nextjs-shadcn/src/app/globals.css +++ /dev/null @@ -1,122 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); -} - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/packages/cli/template/nextjs-shadcn/src/app/layout.tsx b/packages/cli/template/nextjs-shadcn/src/app/layout.tsx deleted file mode 100644 index 5bfe2491..00000000 --- a/packages/cli/template/nextjs-shadcn/src/app/layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; -import Providers from "@/components/providers"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "My ProofKit App", - description: "Generated by the ProofKit CLI", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/packages/cli/template/nextjs-shadcn/src/app/navigation.tsx b/packages/cli/template/nextjs-shadcn/src/app/navigation.tsx deleted file mode 100644 index 01098ac6..00000000 --- a/packages/cli/template/nextjs-shadcn/src/app/navigation.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { ProofKitRoute } from "./proofkit-route"; - -export const primaryRoutes: ProofKitRoute[] = [ - { - label: "Dashboard", - type: "link", - href: "/", - exactMatch: true, - }, -]; - -export const secondaryRoutes: ProofKitRoute[] = []; diff --git a/packages/cli/template/nextjs-shadcn/src/app/proofkit-route.ts b/packages/cli/template/nextjs-shadcn/src/app/proofkit-route.ts deleted file mode 100644 index c8353858..00000000 --- a/packages/cli/template/nextjs-shadcn/src/app/proofkit-route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type React from "react"; - -interface RouteLink { - label: string; - type: "link"; - href: string; - icon?: React.ReactNode; - exactMatch?: boolean; -} - -interface RouteFunction { - label: string; - type: "function"; - icon?: React.ReactNode; - onClick: () => void; - exactMatch?: boolean; -} - -export type ProofKitRoute = RouteLink | RouteFunction; diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppLogo.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppLogo.tsx deleted file mode 100644 index 51a02511..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppLogo.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { InfinityIcon } from "lucide-react"; - -export default function AppLogo() { - return ; -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx deleted file mode 100644 index 0fcd42b1..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type React from "react"; -import { Header } from "@/components/AppShell/internal/Header"; -import { headerHeight } from "./config"; - -export default function MainAppShell({ - children, -}: { - children: React.ReactNode; -}) { - return ( -
-
-
-
-
- {children} -
-
- ); -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css deleted file mode 100644 index d325e4f6..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.header { - margin-bottom: 7.5rem; - background-color: var(--pk-header-bg, transparent); - border-bottom: 1px solid var(--pk-border, rgba(0, 0, 0, 0.08)); -} - -.inner { - display: flex; - justify-content: space-between; - align-items: center; -} - -.link { - display: block; - line-height: 1; - padding: 0.5rem 0.75rem; - border-radius: 0.375rem; - text-decoration: none; - color: inherit; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - background: none; - border: none; -} - -.link:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -[data-theme="dark"] .link:hover { - background-color: rgba(255, 255, 255, 0.06); -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx deleted file mode 100644 index 4f9bdd6b..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import SlotHeaderCenter from "../slot-header-center"; -import SlotHeaderLeft from "../slot-header-left"; -import SlotHeaderRight from "../slot-header-right"; -import classes from "./Header.module.css"; -import HeaderMobileMenu from "./HeaderMobileMenu"; - -export function Header() { - return ( -
-
-
- -
- -
-
- -
-
- -
-
-
-
- ); -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx deleted file mode 100644 index 9f67c6a1..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import { useState } from "react"; -import SlotHeaderMobileMenuContent from "../slot-header-mobile-content"; - -export default function HeaderMobileMenu() { - const [opened, setOpened] = useState(false); - - return ( -
- - {opened && ( -
- setOpened(false)} /> -
- )} -
- ); -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx deleted file mode 100644 index 7b00badb..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { usePathname } from "next/navigation"; - -import type { ProofKitRoute } from "@/app/proofkit-route"; -import classes from "./Header.module.css"; - -export default function HeaderNavLink(route: ProofKitRoute) { - const pathname = usePathname(); - - if (route.type === "function") { - return ( - - ); - } - - const isActive = route.exactMatch - ? pathname === route.href - : pathname.startsWith(route.href); - - if (route.type === "link") { - return ( - - {route.label} - - ); - } -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/config.ts b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/config.ts deleted file mode 100644 index ded639d0..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/config.ts +++ /dev/null @@ -1 +0,0 @@ -export const headerHeight = 56; diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx deleted file mode 100644 index 626c19ff..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderCenter() { - return null; -} - -export default SlotHeaderCenter; diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx deleted file mode 100644 index e0d22824..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Link from "next/link"; - -import AppLogo from "../AppLogo"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects this file to exist and - * may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderLeft() { - return ( - <> - - - - - ); -} - -export default SlotHeaderLeft; diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx deleted file mode 100644 index f8fb4ce9..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { primaryRoutes } from "@/app/navigation"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderMobileMenuContent({ - closeMenu, -}: { - closeMenu: () => void; -}) { - const router = useRouter(); - return ( -
- {primaryRoutes.map((route) => ( - - ))} -
- ); -} - -export default SlotHeaderMobileMenuContent; diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx deleted file mode 100644 index 1b29ee5c..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { primaryRoutes } from "@/app/navigation"; -import { ModeToggle } from "../mode-toggle"; -import HeaderNavLink from "./internal/HeaderNavLink"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderRight() { - return ( -
- {primaryRoutes.map((route) => ( - - ))} - -
- ); -} - -export default SlotHeaderRight; diff --git a/packages/cli/template/nextjs-shadcn/src/components/mode-toggle.tsx b/packages/cli/template/nextjs-shadcn/src/components/mode-toggle.tsx deleted file mode 100644 index 0ca5aa71..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/mode-toggle.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { Moon, Sun } from "lucide-react"; -import { useTheme } from "next-themes"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -export function ModeToggle() { - const { setTheme } = useTheme(); - - return ( - - - - - - setTheme("light")}> - Light - - setTheme("dark")}> - Dark - - setTheme("system")}> - System - - - - ); -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/providers.tsx b/packages/cli/template/nextjs-shadcn/src/components/providers.tsx deleted file mode 100644 index 12d000c8..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/providers.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { ThemeProvider } from "./theme-provider"; -import { Toaster } from "./ui/sonner"; - -export default function Providers({ children }: { children: React.ReactNode }) { - return ( - - {children} - - - ); -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/theme-provider.tsx b/packages/cli/template/nextjs-shadcn/src/components/theme-provider.tsx deleted file mode 100644 index 7c8090f8..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/theme-provider.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import { ThemeProvider as NextThemesProvider } from "next-themes"; -import type * as React from "react"; - -export function ThemeProvider({ - children, - ...props -}: React.ComponentProps) { - return {children}; -} diff --git a/packages/cli/template/nextjs-shadcn/src/components/ui/button.tsx b/packages/cli/template/nextjs-shadcn/src/components/ui/button.tsx deleted file mode 100644 index 0597094f..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/ui/button.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Slot } from "@radix-ui/react-slot"; -import type { VariantProps } from "class-variance-authority"; -import { cva } from "class-variance-authority"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - defaultVariants: { - size: "default", - variant: "default", - }, - variants: { - size: { - default: "h-9 px-4 py-2", - icon: "h-9 w-9", - lg: "h-10 rounded-md px-8", - sm: "h-8 rounded-md px-3 text-xs", - }, - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - }, - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; -} - -function Button({ - className, - variant, - size, - asChild = false, - ref, - ...props -}: ButtonProps & { ref?: React.Ref }) { - const Comp = asChild ? Slot : "button"; - return ( - - ); -} - -export { Button, buttonVariants }; diff --git a/packages/cli/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx b/packages/cli/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index 34137647..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,265 +0,0 @@ -"use client"; - -import { Check, ChevronRight, Circle } from "lucide-react"; -import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function DropdownMenu({ - ...props -}: React.ComponentProps) { - return ; -} - -function DropdownMenuPortal({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuContent({ - className, - sideOffset = 4, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function DropdownMenuGroup({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuItem({ - className, - inset, - variant, - ...props -}: React.ComponentProps & { - inset?: boolean; - variant?: "destructive"; -}) { - return ( - - ); -} - -function DropdownMenuCheckboxItem({ - className, - children, - checked, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function DropdownMenuRadioItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function DropdownMenuLabel({ - className, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean; -}) { - return ( - - ); -} - -function DropdownMenuRadioGroup({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuShortcut({ - className, - ...props -}: React.HTMLAttributes) { - return ( - - ); -} - -function DropdownMenuSub({ - ...props -}: React.ComponentProps) { - return ; -} - -function DropdownMenuSubTrigger({ - className, - inset, - children, - ...props -}: React.ComponentProps & { - inset?: boolean; -}) { - return ( - svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 [&_svg:not([role=img]):not([class*=text-])]:opacity-60", - inset && "ps-8", - className, - )} - data-slot="dropdown-menu-sub-trigger" - {...props} - > - {children} - - - ); -} - -function DropdownMenuSubContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuPortal, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -}; diff --git a/packages/cli/template/nextjs-shadcn/src/components/ui/sonner.tsx b/packages/cli/template/nextjs-shadcn/src/components/ui/sonner.tsx deleted file mode 100644 index 81df26ae..00000000 --- a/packages/cli/template/nextjs-shadcn/src/components/ui/sonner.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { useTheme } from "next-themes"; -import { Toaster as Sonner } from "sonner"; - -type ToasterProps = React.ComponentProps; - -function Toaster({ ...props }: ToasterProps) { - const { theme = "system" } = useTheme(); - - return ( - - ); -} - -export { Toaster }; diff --git a/packages/cli/template/nextjs-shadcn/src/lib/env.ts b/packages/cli/template/nextjs-shadcn/src/lib/env.ts deleted file mode 100644 index ba4e58ce..00000000 --- a/packages/cli/template/nextjs-shadcn/src/lib/env.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod/v4"; - -export const env = createEnv({ - server: { - NODE_ENV: z - .enum(["development", "test", "production"]) - .catch("development"), - }, - client: {}, - experimental__runtimeEnv: {}, -}); diff --git a/packages/cli/template/nextjs-shadcn/src/lib/utils.ts b/packages/cli/template/nextjs-shadcn/src/lib/utils.ts deleted file mode 100644 index d39aff9e..00000000 --- a/packages/cli/template/nextjs-shadcn/src/lib/utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ClassValue } from "clsx"; -import { clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/cli/template/nextjs-shadcn/tsconfig.json b/packages/cli/template/nextjs-shadcn/tsconfig.json deleted file mode 100644 index c412e057..00000000 --- a/packages/cli/template/nextjs-shadcn/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - }, - "strictNullChecks": true - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/packages/cli/template/pages/nextjs/blank/page.tsx b/packages/cli/template/pages/nextjs/blank/page.tsx deleted file mode 100644 index 68a77ea1..00000000 --- a/packages/cli/template/pages/nextjs/blank/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export default function BlankPage() { - return
BlankPage
; -} diff --git a/packages/cli/template/pages/nextjs/table-edit/actions.ts b/packages/cli/template/pages/nextjs/table-edit/actions.ts deleted file mode 100644 index 662a1871..00000000 --- a/packages/cli/template/pages/nextjs/table-edit/actions.ts +++ /dev/null @@ -1,24 +0,0 @@ -"use server"; - -import { __ZOD_TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { __ACTION_CLIENT__ } from "@/server/safe-action"; - -import { idFieldName } from "./schema"; - -export const updateRecord = __ACTION_CLIENT__ - .inputSchema(__ZOD_TYPE_NAME__.partial()) - .action(async ({ parsedInput }) => { - const id = parsedInput[idFieldName]; - delete parsedInput[idFieldName]; // this ensures the id field value is not included in the updated fieldData - const data = parsedInput; - - const { - data: { recordId }, - } = await __CLIENT_NAME__.findOne({ query: { [idFieldName]: `==${id}` } }); - - return await __CLIENT_NAME__.update({ - recordId, - fieldData: data, - }); - }); diff --git a/packages/cli/template/pages/nextjs/table-edit/page.tsx b/packages/cli/template/pages/nextjs/table-edit/page.tsx deleted file mode 100644 index efdbd80b..00000000 --- a/packages/cli/template/pages/nextjs/table-edit/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Code, Stack, Text } from "@mantine/core"; -import React from "react"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; - -import { idFieldName } from "./schema"; -import TableContent from "./table"; - -export default async function TablePage() { - // this function is limited to 100 records by default. To load more, see the other table templates from the docs - const { data } = await __CLIENT_NAME__.list({ - fetch: { next: { revalidate: 60 } }, // only call the database at most once every 60 seconds - }); - return ( - -
- - This table allows editing. Double-click on a cell to edit the value. - - - NOTE: This feature requires a primary key field on your API layout. If - your primary key field is not {idFieldName}, update the - idFieldName variable in the schema.ts file. - -
- d.fieldData)} /> -
- ); -} diff --git a/packages/cli/template/pages/nextjs/table-edit/schema.ts b/packages/cli/template/pages/nextjs/table-edit/schema.ts deleted file mode 100644 index ebfd964a..00000000 --- a/packages/cli/template/pages/nextjs/table-edit/schema.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; - -// TODO: Make sure this variable is properly set to your primary key field -export const idFieldName: keyof __TYPE_NAME__ = "__FIRST_FIELD_NAME__"; diff --git a/packages/cli/template/pages/nextjs/table-edit/table.tsx b/packages/cli/template/pages/nextjs/table-edit/table.tsx deleted file mode 100644 index fb7b6f2c..00000000 --- a/packages/cli/template/pages/nextjs/table-edit/table.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { - MantineReactTable, - type MRT_Cell, - type MRT_ColumnDef, - useMantineReactTable, -} from "mantine-react-table"; -import React from "react"; -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { showErrorNotification } from "@/utils/notification-helpers"; - -import { updateRecord } from "./actions"; -import { idFieldName } from "./schema"; - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -async function handleSaveCell(cell: MRT_Cell, value: unknown) { - const resp = await updateRecord({ - [idFieldName]: cell.row.id, - [cell.column.id]: value, - }); - if (!resp?.data) { - showErrorNotification("Failed to update record"); - } -} - -export default function MyTable({ data }: { data: TData[] }) { - const table = useMantineReactTable({ - data, - columns, - enableEditing: true, - editDisplayMode: "cell", - getRowId: (row) => row[idFieldName], - mantineEditTextInputProps: ({ cell }) => ({ - //onBlur is more efficient, but could use onChange instead - onBlur: (event) => { - handleSaveCell(cell, event.target.value); - }, - }), - }); - return ; -} diff --git a/packages/cli/template/pages/nextjs/table-infinite-edit/actions.ts b/packages/cli/template/pages/nextjs/table-infinite-edit/actions.ts deleted file mode 100644 index 4c3f9a70..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite-edit/actions.ts +++ /dev/null @@ -1,83 +0,0 @@ -"use server"; - -import type { clientTypes } from "@proofkit/fmdapi"; -import dayjs from "dayjs"; -import { z } from "zod/v4"; -import { - type __TYPE_NAME__, - __ZOD_TYPE_NAME__, -} from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { __ACTION_CLIENT__ } from "@/server/safe-action"; - -import { idFieldName } from "./schema"; - -const limit = 50; // raise or lower this number depending on how your layout performs -export const fetchData = __ACTION_CLIENT__ - .inputSchema( - z.object({ - offset: z.number().catch(0), - sorting: z.array( - z.object({ id: z.string(), desc: z.boolean().default(false) }), - ), - columnFilters: z.array(z.object({ id: z.string(), value: z.unknown() })), - }), - ) - .action(async ({ parsedInput: { offset, sorting, columnFilters } }) => { - const getOptions: clientTypes.ListParams<__TYPE_NAME__, any> & { - query: clientTypes.Query<__TYPE_NAME__>[]; - } = { - limit, - offset, - query: [{ ["__FIRST_FIELD_NAME__"]: "*" }], - }; - - if (sorting.length > 0) { - getOptions.sort = sorting.map(({ id, desc }) => ({ - fieldName: id as keyof __TYPE_NAME__, - sortOrder: desc ? "descend" : "ascend", - })); - } - - if (columnFilters.length > 0) { - getOptions.query = columnFilters - .map(({ id, value }) => { - if (typeof value === "string") { - return { - [id]: value, - }; - } else if (typeof value === "object" && value instanceof Date) { - return { - [id]: dayjs(value).format("YYYY+MM+DD"), - }; - } - return null; - }) - .filter(Boolean) as clientTypes.Query[]; - } - - const data = await __CLIENT_NAME__.find(getOptions); - - return { - data: data.data, - hasNextPage: data.dataInfo.foundCount > limit + offset, - totalCount: data.dataInfo.foundCount, - }; - }); - -export const updateRecord = __ACTION_CLIENT__ - .inputSchema(__ZOD_TYPE_NAME__.partial()) - .action(async ({ parsedInput }) => { - const id = parsedInput[idFieldName]; - delete parsedInput[idFieldName]; // this ensures the id field value is not included in the updated fieldData - const data = parsedInput; - - const { - data: { recordId }, - } = await __CLIENT_NAME__.findOne({ query: { [idFieldName]: `==${id}` } }); - - return await __CLIENT_NAME__.update({ - recordId, - fieldData: data, - }); - }); diff --git a/packages/cli/template/pages/nextjs/table-infinite-edit/page.tsx b/packages/cli/template/pages/nextjs/table-infinite-edit/page.tsx deleted file mode 100644 index 1fcf0f2a..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite-edit/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Code, Stack, Text } from "@mantine/core"; -import React from "react"; - -import { idFieldName } from "./schema"; -import TableContent from "./table"; - -export default async function TablePage() { - return ( - -
- - This table allows editing. Double-click on a cell to edit the value. - - - NOTE: This feature requires a primary key field on your API layout. If - your primary key field is not {idFieldName}, update the - idFieldName variable in the schema.ts file. - -
- -
- ); -} diff --git a/packages/cli/template/pages/nextjs/table-infinite-edit/query.ts b/packages/cli/template/pages/nextjs/table-infinite-edit/query.ts deleted file mode 100644 index 4facb540..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite-edit/query.ts +++ /dev/null @@ -1,87 +0,0 @@ -"use client"; - -import { - useInfiniteQuery, - useMutation, - useQueryClient, -} from "@tanstack/react-query"; -import type { - MRT_ColumnFiltersState, - MRT_SortingState, -} from "mantine-react-table"; -import { useMemo } from "react"; -import { showErrorNotification } from "@/utils/notification-helpers"; - -import { fetchData, updateRecord } from "./actions"; -import { idFieldName } from "./schema"; - -export function useAllData({ - sorting, - columnFilters, -}: { - sorting: MRT_SortingState; - columnFilters: MRT_ColumnFiltersState; -}) { - const queryKey = ["all-__SCHEMA_NAME__", sorting, columnFilters]; - // useInfiniteQuery is used to help with automatic pagination - const qr = useInfiniteQuery({ - queryKey, - queryFn: async ({ pageParam: offset }) => { - const result = await fetchData({ offset, sorting, columnFilters }); - if (!result) throw new Error(`Failed to fetch __SCHEMA_NAME__`); - if (!result.data) throw new Error(`No data found for __SCHEMA_NAME__`); - return result?.data; - }, - retry: false, - initialPageParam: 0, - getNextPageParam: (lastPage, pages) => - lastPage.hasNextPage - ? pages.flatMap((page) => page.data).length - : undefined, - }); - - const flatData = useMemo( - () => - qr.data?.pages.flatMap((page) => page.data).map((o) => o.fieldData) ?? [], - [qr.data], - ); - const totalFetched = flatData.length; - const totalDBRowCount = qr.data?.pages?.[0]?.totalCount ?? 0; - - const queryClient = useQueryClient(); - - const updateRecordMutation = useMutation({ - mutationFn: updateRecord, - onMutate: async (newRecord) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ queryKey }); - - // Optimistically update to the new value - queryClient.setQueryData(queryKey, (old) => { - if (!old) return old; - return { - ...old, - pages: old.pages.map((page) => ({ - ...page, - data: page.data.map((row) => - row.fieldData[idFieldName] === newRecord[idFieldName] - ? { ...row, fieldData: { ...row.fieldData, ...newRecord } } - : row, - ), - })), - }; - }); - }, - onError: () => { - showErrorNotification("Failed to update record"); - }, - }); - - return { - ...qr, - data: flatData, - totalDBRowCount, - totalFetched, - updateRecord: updateRecordMutation.mutate, - }; -} diff --git a/packages/cli/template/pages/nextjs/table-infinite-edit/schema.ts b/packages/cli/template/pages/nextjs/table-infinite-edit/schema.ts deleted file mode 100644 index ebfd964a..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite-edit/schema.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; - -// TODO: Make sure this variable is properly set to your primary key field -export const idFieldName: keyof __TYPE_NAME__ = "__FIRST_FIELD_NAME__"; diff --git a/packages/cli/template/pages/nextjs/table-infinite-edit/table.tsx b/packages/cli/template/pages/nextjs/table-infinite-edit/table.tsx deleted file mode 100644 index 33ecc573..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite-edit/table.tsx +++ /dev/null @@ -1,130 +0,0 @@ -"use client"; - -import { Text } from "@mantine/core"; -import { - createMRTColumnHelper, - MantineReactTable, - type MRT_Cell, - type MRT_ColumnDef, - type MRT_ColumnFiltersState, - type MRT_RowVirtualizer, - type MRT_SortingState, - useMantineReactTable, -} from "mantine-react-table"; -import React, { - type UIEvent, - useCallback, - useEffect, - useRef, - useState, -} from "react"; -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; - -import { useAllData } from "./query"; -import { idFieldName } from "./schema"; - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -export default function MyTable() { - const tableContainerRef = useRef(null); - const rowVirtualizerInstanceRef = - useRef>(null); - - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState( - [], - ); - - const { - data, - totalDBRowCount, - totalFetched, - isLoading, - isFetching, - fetchNextPage, - updateRecord, - } = useAllData({ sorting, columnFilters }); - - async function handleSaveCell(cell: MRT_Cell, value: unknown) { - updateRecord({ - [idFieldName]: cell.row.id, - [cell.column.id]: value, - }); - } - - const table = useMantineReactTable({ - data, - columns, - rowCount: totalDBRowCount, - enableRowVirtualization: true, // only render the rows that are visible on screen to improve performance - state: { isLoading, sorting, showProgressBars: isFetching, columnFilters }, - enableGlobalFilter: false, // doesn't work as easily with server-side filters, it's better to filter the specific columns - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - enablePagination: false, // hide pagination buttons - enableStickyHeader: true, - mantineBottomToolbarProps: { style: { alignItems: "center" } }, - renderBottomToolbarCustomActions: () => - !isLoading ? ( - - Fetched {totalFetched} of {totalDBRowCount} - - ) : null, - mantineTableContainerProps: ({ table }) => { - return { - h: table.getState().isFullScreen - ? "100%" - : `calc(100vh - var(--app-shell-header-height) - 13rem)`, // may need to adjust this height if you have more elements on your page - ref: tableContainerRef, - onScroll: ( - event: UIEvent, //add an event listener to the table container element - ) => fetchMoreOnBottomReached(event.target as HTMLDivElement), - }; - }, - - /** Inline editing functionality */ - enableEditing: true, - editDisplayMode: "cell", - getRowId: (row) => row[idFieldName], - mantineEditTextInputProps: ({ cell }) => ({ - // onBlur is more efficient (only called when you leave the field) - // onChange event could be used for other types of edits, like dropdowns - onBlur: (event) => { - handleSaveCell(cell, event.target.value); - }, - }), - }); - - // called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table - const fetchMoreOnBottomReached = useCallback( - (containerRefElement?: HTMLDivElement | null) => { - if (containerRefElement) { - const { scrollHeight, scrollTop, clientHeight } = containerRefElement; - // once the user has scrolled within 400px of the bottom of the table, fetch more data - if ( - scrollHeight - scrollTop - clientHeight < 400 && - !isFetching && - totalFetched < totalDBRowCount - ) { - void fetchNextPage(); - } - } - }, - [fetchNextPage, isFetching, totalFetched, totalDBRowCount], - ); - - // scroll to top of table when sorting or filters change - useEffect(() => { - if (rowVirtualizerInstanceRef.current) { - try { - rowVirtualizerInstanceRef.current.scrollToIndex(0); - } catch (e) { - console.error(e); - } - } - }, [sorting, columnFilters]); - - return ; -} diff --git a/packages/cli/template/pages/nextjs/table-infinite/actions.ts b/packages/cli/template/pages/nextjs/table-infinite/actions.ts deleted file mode 100644 index dcd7d304..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite/actions.ts +++ /dev/null @@ -1,61 +0,0 @@ -"use server"; - -import type { clientTypes } from "@proofkit/fmdapi"; -import dayjs from "dayjs"; -import { z } from "zod/v4"; -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; -import { __ACTION_CLIENT__ } from "@/server/safe-action"; - -const limit = 50; // raise or lower this number depending on how your layout performs -export const fetchData = __ACTION_CLIENT__ - .inputSchema( - z.object({ - offset: z.number().catch(0), - sorting: z.array( - z.object({ id: z.string(), desc: z.boolean().default(false) }), - ), - columnFilters: z.array(z.object({ id: z.string(), value: z.unknown() })), - }), - ) - .action(async ({ parsedInput: { offset, sorting, columnFilters } }) => { - const getOptions: clientTypes.ListParams<__TYPE_NAME__, any> & { - query: clientTypes.Query<__TYPE_NAME__>[]; - } = { - limit, - offset, - query: [{ ["__FIRST_FIELD_NAME__"]: "*" }], - }; - - if (sorting.length > 0) { - getOptions.sort = sorting.map(({ id, desc }) => ({ - fieldName: id as keyof __TYPE_NAME__, - sortOrder: desc ? "descend" : "ascend", - })); - } - - if (columnFilters.length > 0) { - getOptions.query = columnFilters - .map(({ id, value }) => { - if (typeof value === "string") { - return { - [id]: value, - }; - } else if (typeof value === "object" && value instanceof Date) { - return { - [id]: dayjs(value).format("YYYY+MM+DD"), - }; - } - return null; - }) - .filter(Boolean) as clientTypes.Query[]; - } - - const data = await __CLIENT_NAME__.find(getOptions); - - return { - data: data.data, - hasNextPage: data.dataInfo.foundCount > limit + offset, - totalCount: data.dataInfo.foundCount, - }; - }); diff --git a/packages/cli/template/pages/nextjs/table-infinite/page.tsx b/packages/cli/template/pages/nextjs/table-infinite/page.tsx deleted file mode 100644 index 2c6b6046..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Stack } from "@mantine/core"; - -import MyTable from "./table"; - -export default async function TablePage() { - return ( - - - - ); -} diff --git a/packages/cli/template/pages/nextjs/table-infinite/query.ts b/packages/cli/template/pages/nextjs/table-infinite/query.ts deleted file mode 100644 index c3f618e5..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite/query.ts +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { useInfiniteQuery } from "@tanstack/react-query"; -import type { - MRT_ColumnFiltersState, - MRT_SortingState, -} from "mantine-react-table"; -import { useMemo } from "react"; - -import { fetchData } from "./actions"; - -export function useAllData({ - sorting, - columnFilters, -}: { - sorting: MRT_SortingState; - columnFilters: MRT_ColumnFiltersState; -}) { - // useInfiniteQuery is used to help with automatic pagination - const qr = useInfiniteQuery({ - queryKey: ["all-__SCHEMA_NAME__", sorting, columnFilters], - queryFn: async ({ pageParam: offset }) => { - const result = await fetchData({ offset, sorting, columnFilters }); - if (!result) throw new Error(`Failed to fetch __SCHEMA_NAME__`); - if (!result.data) throw new Error(`No data found for __SCHEMA_NAME__`); - return result?.data; - }, - retry: false, - initialPageParam: 0, - getNextPageParam: (lastPage, pages) => - lastPage.hasNextPage - ? pages.flatMap((page) => page.data).length - : undefined, - }); - - const flatData = useMemo( - () => - qr.data?.pages.flatMap((page) => page.data).map((o) => o.fieldData) ?? [], - [qr.data], - ); - const totalFetched = flatData.length; - const totalDBRowCount = qr.data?.pages?.[0]?.totalCount ?? 0; - - return { ...qr, data: flatData, totalDBRowCount, totalFetched }; -} diff --git a/packages/cli/template/pages/nextjs/table-infinite/table.tsx b/packages/cli/template/pages/nextjs/table-infinite/table.tsx deleted file mode 100644 index e82020cf..00000000 --- a/packages/cli/template/pages/nextjs/table-infinite/table.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import { Text } from "@mantine/core"; -import { - createMRTColumnHelper, - MantineReactTable, - type MRT_ColumnDef, - type MRT_ColumnFiltersState, - type MRT_RowVirtualizer, - type MRT_SortingState, - useMantineReactTable, -} from "mantine-react-table"; -import React, { - type UIEvent, - useCallback, - useEffect, - useRef, - useState, -} from "react"; -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; - -import { useAllData } from "./query"; - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -export default function MyTable() { - const tableContainerRef = useRef(null); - const rowVirtualizerInstanceRef = - useRef>(null); - - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState( - [], - ); - - const { - data, - totalDBRowCount, - totalFetched, - isLoading, - isFetching, - fetchNextPage, - } = useAllData({ sorting, columnFilters }); - - const table = useMantineReactTable({ - data, - columns, - rowCount: totalDBRowCount, - enableRowVirtualization: true, // only render the rows that are visible on screen to improve performance - state: { isLoading, sorting, showProgressBars: isFetching, columnFilters }, - enableGlobalFilter: false, // doesn't work as easily with server-side filters, it's better to filter the specific columns - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - enablePagination: false, // hide pagination buttons - enableStickyHeader: true, - mantineBottomToolbarProps: { style: { alignItems: "center" } }, - renderBottomToolbarCustomActions: () => - !isLoading ? ( - - Fetched {totalFetched} of {totalDBRowCount} - - ) : null, - mantineTableContainerProps: ({ table }) => { - return { - h: table.getState().isFullScreen - ? "100%" - : `calc(100vh - var(--app-shell-header-height) - 10rem)`, // may need to adjust this height if you have more elements on your page - ref: tableContainerRef, - onScroll: ( - event: UIEvent, //add an event listener to the table container element - ) => fetchMoreOnBottomReached(event.target as HTMLDivElement), - }; - }, - }); - - // called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table - const fetchMoreOnBottomReached = useCallback( - (containerRefElement?: HTMLDivElement | null) => { - if (containerRefElement) { - const { scrollHeight, scrollTop, clientHeight } = containerRefElement; - // once the user has scrolled within 400px of the bottom of the table, fetch more data - if ( - scrollHeight - scrollTop - clientHeight < 400 && - !isFetching && - totalFetched < totalDBRowCount - ) { - void fetchNextPage(); - } - } - }, - [fetchNextPage, isFetching, totalFetched, totalDBRowCount], - ); - - // scroll to top of table when sorting or filters change - useEffect(() => { - if (rowVirtualizerInstanceRef.current) { - try { - rowVirtualizerInstanceRef.current.scrollToIndex(0); - } catch (e) { - console.error(e); - } - } - }, [sorting, columnFilters]); - - return ; -} diff --git a/packages/cli/template/pages/nextjs/table/page.tsx b/packages/cli/template/pages/nextjs/table/page.tsx deleted file mode 100644 index b74695f9..00000000 --- a/packages/cli/template/pages/nextjs/table/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Stack } from "@mantine/core"; -import React from "react"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; - -import TableContent from "./table"; - -export default async function TablePage() { - // this function is limited to 100 records by default. To load more, see the other table templates from the docs - const { data } = await __CLIENT_NAME__.list({ - fetch: { next: { revalidate: 60 } }, // only call the database at most once every 60 seconds - }); - return ( - - d.fieldData)} /> - - ); -} diff --git a/packages/cli/template/pages/nextjs/table/table.tsx b/packages/cli/template/pages/nextjs/table/table.tsx deleted file mode 100644 index f94ff3d8..00000000 --- a/packages/cli/template/pages/nextjs/table/table.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { - MantineReactTable, - type MRT_ColumnDef, - useMantineReactTable, -} from "mantine-react-table"; -import React from "react"; -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -export default function MyTable({ data }: { data: TData[] }) { - const table = useMantineReactTable({ data, columns }); - return ; -} diff --git a/packages/cli/template/pages/vite-wv/blank/index.tsx b/packages/cli/template/pages/vite-wv/blank/index.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/cli/template/pages/vite-wv/table-edit/index.tsx b/packages/cli/template/pages/vite-wv/table-edit/index.tsx deleted file mode 100644 index d69e5a40..00000000 --- a/packages/cli/template/pages/vite-wv/table-edit/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Code, Stack, Text } from "@mantine/core"; -import { createFileRoute } from "@tanstack/react-router"; -import { - MantineReactTable, - type MRT_Cell, - type MRT_ColumnDef, - useMantineReactTable, -} from "mantine-react-table"; -import FullScreenLoader from "@/components/full-screen-loader"; -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; - -export const Route = createFileRoute("/")({ - component: RouteComponent, - pendingComponent: () => , - loader: async () => { - // this function is limited to 100 records by default. To load more, see the other table templates from the docs - const { data } = await __CLIENT_NAME__.list(); - return data.map((record) => record.fieldData) as __TYPE_NAME__[]; - }, -}); - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -// TODO: Make sure this variable is properly set to your primary key field -const idFieldName: keyof __TYPE_NAME__ = "__FIRST_FIELD_NAME__"; -async function handleSaveCell(cell: MRT_Cell, value: unknown) { - const { - data: { recordId }, - } = await __CLIENT_NAME__.findOne({ - query: { [idFieldName]: `==${cell.row.id}` }, - }); - - await __CLIENT_NAME__.update({ - fieldData: { [cell.column.id]: value }, - recordId, - }); -} - -function RouteComponent() { - const data = Route.useLoaderData(); - const table = useMantineReactTable({ - data, - columns, - enableEditing: true, - editDisplayMode: "cell", - getRowId: (row) => row[idFieldName], - mantineEditTextInputProps: ({ cell }) => ({ - //onBlur is more efficient, but could use onChange instead - onBlur: (event) => { - handleSaveCell(cell, event.target.value); - }, - }), - }); - return ( - -
- - This table allows editing. Double-click on a cell to edit the value. - - - NOTE: This feature requires a primary key field on your API layout. If - your primary key field is not {idFieldName}, update the - idFieldName variable in the code. - -
- -
- ); -} diff --git a/packages/cli/template/pages/vite-wv/table/index.tsx b/packages/cli/template/pages/vite-wv/table/index.tsx deleted file mode 100644 index 0f16f1eb..00000000 --- a/packages/cli/template/pages/vite-wv/table/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Stack, Text } from "@mantine/core"; -import { createFileRoute } from "@tanstack/react-router"; -import { - MantineReactTable, - type MRT_ColumnDef, - useMantineReactTable, -} from "mantine-react-table"; -import FullScreenLoader from "@/components/full-screen-loader"; -import type { __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__"; -import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client"; - -export const Route = createFileRoute("/")({ - component: RouteComponent, - pendingComponent: () => , - loader: async () => { - // this function is limited to 100 records by default. To load more, see the other table templates from the docs - const { data } = await __CLIENT_NAME__.list(); - return data.map((record) => record.fieldData) as __TYPE_NAME__[]; - }, -}); - -type TData = __TYPE_NAME__; - -const columns: MRT_ColumnDef[] = []; - -function RouteComponent() { - const data = Route.useLoaderData(); - const table = useMantineReactTable({ data, columns }); - return ( - - This basic table loads up to 100 records by default - - - ); -} diff --git a/packages/cli/template/vite-wv/.claude/launch.json b/packages/cli/template/vite-wv/.claude/launch.json deleted file mode 100644 index 9da2f24c..00000000 --- a/packages/cli/template/vite-wv/.claude/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "configurations": [ - { - "name": "Preview", - "runtimeExecutable": "__PACKAGE_MANAGER__", - "runtimeArgs": ["run", "dev"], - "cwd": "${workspaceFolder}", - "autoPort": true, - "port": 5175 - }, - { - "name": "Typegen", - "runtimeExecutable": "__PACKAGE_MANAGER__", - "runtimeArgs": ["run", "typegen"], - "cwd": "${workspaceFolder}" - } - ] -} diff --git a/packages/cli/template/vite-wv/.vscode/settings.json b/packages/cli/template/vite-wv/.vscode/settings.json deleted file mode 100644 index a5253510..00000000 --- a/packages/cli/template/vite-wv/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "files.watcherExclude": { - "**/routeTree.gen.ts": true - }, - "search.exclude": { - "**/routeTree.gen.ts": true - }, - "files.readonlyInclude": { - "**/routeTree.gen.ts": true - } -} diff --git a/packages/cli/template/vite-wv/AGENTS.md b/packages/cli/template/vite-wv/AGENTS.md deleted file mode 100644 index 6b2924ba..00000000 --- a/packages/cli/template/vite-wv/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -__AGENT_INSTRUCTIONS__ diff --git a/packages/cli/template/vite-wv/CLAUDE.md b/packages/cli/template/vite-wv/CLAUDE.md deleted file mode 100644 index 43c994c2..00000000 --- a/packages/cli/template/vite-wv/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/packages/cli/template/vite-wv/_gitignore b/packages/cli/template/vite-wv/_gitignore deleted file mode 100644 index b02a5be6..00000000 --- a/packages/cli/template/vite-wv/_gitignore +++ /dev/null @@ -1,20 +0,0 @@ -# Local -.DS_Store -*.local -*.log* -.env* - -# Dist -node_modules -.pnpm-store -dist/ -.vinxi -.output -.vercel -.netlify -.wrangler - -# IDE -.vscode/* -!.vscode/extensions.json -.idea diff --git a/packages/cli/template/vite-wv/components.json b/packages/cli/template/vite-wv/components.json deleted file mode 100644 index 802bb88a..00000000 --- a/packages/cli/template/vite-wv/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/index.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/packages/cli/template/vite-wv/index.html b/packages/cli/template/vite-wv/index.html deleted file mode 100644 index db0fcdc2..00000000 --- a/packages/cli/template/vite-wv/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - ProofKit Web Viewer Starter - - - -
- - - diff --git a/packages/cli/template/vite-wv/package.json b/packages/cli/template/vite-wv/package.json deleted file mode 100644 index 736fcfb5..00000000 --- a/packages/cli/template/vite-wv/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "webviewer-demo", - "version": "0.0.0", - "private": true, - "type": "module", - "engines": { - "node": "^22.12.0 || ^24.0.0" - }, - "scripts": { - "build": "vite build", - "build:upload": "__PNPM_COMMAND__ build && __PNPM_COMMAND__ upload", - "dev": "vite", - "serve": "vite preview", - "start": "vite", - "typegen": "__PNPM_EXECUTE_COMMAND__ @proofkit/typegen", - "typegen:ui": "__PNPM_EXECUTE_COMMAND__ @proofkit/typegen ui", - "upload": "node ./scripts/upload.js", - "check": "ultracite check", - "fix": "ultracite fix", - "lint": "ultracite check .", - "format": "ultracite fix .", - "prepare": "husky" - }, - "dependencies": { - "@tanstack/react-query": "^5.90.21", - "@tanstack/react-router": "^1.167.4", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "zod": "^4" - }, - "devDependencies": { - "@proofkit/typegen": "^1.1.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.2.0", - "dotenv": "^17.3.1", - "husky": "^9.1.7", - "lint-staged": "^17.0.5", - "open": "^11.0.0", - "typescript": "^6.0.3", - "ultracite": "^7.0.0", - "vite": "^7.3.3", - "vite-plugin-singlefile": "^2.3.2" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}": [ - "pnpm exec ultracite fix" - ] - } -} diff --git a/packages/cli/template/vite-wv/proofkit-typegen.config.jsonc b/packages/cli/template/vite-wv/proofkit-typegen.config.jsonc deleted file mode 100644 index 3d1fc759..00000000 --- a/packages/cli/template/vite-wv/proofkit-typegen.config.jsonc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://proofkit.proof.sh/typegen-config-schema.json", - "config": { - "type": "fmdapi", - "path": "./src/config/schemas/filemaker", - "clearOldFiles": true, - "clientSuffix": "Layout", - "validator": "zod/v4", - "webviewerScriptName": "ExecuteDataApi", - "fmMcp": { - "enabled": true - }, - "layouts": [ - // Add layouts here when you're ready to generate clients. - // { "layoutName": "API_Customers", "schemaName": "Customers" } - ] - } -} diff --git a/packages/cli/template/vite-wv/proofkit.json b/packages/cli/template/vite-wv/proofkit.json deleted file mode 100644 index 732063cc..00000000 --- a/packages/cli/template/vite-wv/proofkit.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ui": "shadcn", - "auth": { "type": "none" }, - "envFile": ".env", - "appType": "webviewer", - "dataSources": [], - "replacedMainPage": false, - "registryTemplates": [] -} diff --git a/packages/cli/template/vite-wv/scripts/filemaker.js b/packages/cli/template/vite-wv/scripts/filemaker.js deleted file mode 100644 index 26df24d5..00000000 --- a/packages/cli/template/vite-wv/scripts/filemaker.js +++ /dev/null @@ -1,193 +0,0 @@ -import { resolve } from "node:path"; - -import dotenv from "dotenv"; - -const currentDirectory = import.meta.dirname; -const envPath = resolve(currentDirectory, "../.env"); - -dotenv.config({ path: envPath }); - -const defaultFmMcpBaseUrl = - process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365"; - -const stripFileExtension = (fileName) => fileName.replace(/\.fmp12$/iu, ""); - -const getConnectedFiles = async (baseUrl = defaultFmMcpBaseUrl) => { - const healthResponse = await fetch(`${baseUrl}/health`).catch(() => null); - if (!healthResponse?.ok) { - return []; - } - - const connectedFiles = await fetch(`${baseUrl}/connectedFiles`) - .then((response) => (response.ok ? response.json() : [])) - .catch(() => []); - - return Array.isArray(connectedFiles) ? connectedFiles : []; -}; - -const isBridgeReachable = async (baseUrl = defaultFmMcpBaseUrl) => { - const healthResponse = await fetch(`${baseUrl}/health`).catch(() => null); - return healthResponse?.ok === true; -}; - -const normalizeTarget = (fileName) => - stripFileExtension(fileName).toLowerCase(); - -export const resolveFileMakerTarget = async () => { - const connectedFiles = await getConnectedFiles(); - const targetFromEnv = process.env.FM_DATABASE - ? normalizeTarget(process.env.FM_DATABASE) - : undefined; - - if (targetFromEnv) { - const matches = connectedFiles.filter( - (connectedFile) => normalizeTarget(connectedFile) === targetFromEnv, - ); - if (matches.length === 1) { - return { - fileName: stripFileExtension(matches[0]), - host: "$", - source: "fm-mcp", - }; - } - - if (connectedFiles.length > 0) { - throw new Error( - `FM_DATABASE is set to "${process.env.FM_DATABASE}" but no matching connected file was found via FM MCP.`, - ); - } - } - - if (connectedFiles.length === 1) { - return { - fileName: stripFileExtension(connectedFiles[0]), - host: "$", - source: "fm-mcp", - }; - } - - if (connectedFiles.length > 1) { - throw new Error( - `Multiple FileMaker files are connected via FM MCP (${connectedFiles.join(", ")}). Set FM_DATABASE to choose one.`, - ); - } - - const serverValue = process.env.FM_SERVER; - const databaseValue = process.env.FM_DATABASE; - - if (serverValue && databaseValue) { - let hostname; - try { - ({ hostname } = new URL(serverValue)); - } catch { - hostname = serverValue.replace(/^https?:\/\//u, "").replace(/\/.*$/u, ""); - } - - return { - fileName: stripFileExtension(databaseValue), - host: hostname, - source: "env", - }; - } - - return null; -}; - -export const callFileMakerScript = async ({ - baseUrl = defaultFmMcpBaseUrl, - connectedFileName, - scriptName, - data, -}) => { - const response = await fetch(`${baseUrl}/callScript`, { - body: JSON.stringify({ - connectedFileName, - data, - scriptName, - }), - headers: { "content-type": "application/json" }, - method: "POST", - }).catch((error) => { - throw new Error(`Could not reach FM MCP bridge at ${baseUrl}/callScript.`, { - cause: error, - }); - }); - - const payload = await response.json().catch(() => null); - - if (!response.ok) { - const errorMessage = - payload && typeof payload.error === "string" - ? payload.error - : `HTTP ${response.status} from ${baseUrl}/callScript`; - throw new Error(errorMessage); - } - - if ( - !payload || - typeof payload.fetchId !== "string" || - !("result" in payload) - ) { - throw new Error("Invalid response from FM MCP bridge /callScript."); - } - - return payload; -}; - -export const deployHtml = async ({ - appName, - path, - scriptName = "deploy_html", -}) => { - const target = await resolveFileMakerTarget(); - if (!target) { - return { - method: "none", - target: null, - }; - } - - const payload = { appName, path }; - const bridgeAvailable = - target.source === "fm-mcp" && (await isBridgeReachable()); - - if (bridgeAvailable) { - try { - const bridgeResult = await callFileMakerScript({ - connectedFileName: target.fileName, - data: payload, - scriptName, - }); - return { - method: "bridge", - result: bridgeResult, - target, - }; - } catch (error) { - if (target.host !== "$") { - throw error; - } - } - } - - const parameter = JSON.stringify(payload); - return { - method: "url", - target, - url: buildFmpUrl({ - fileName: target.fileName, - host: target.host, - parameter, - scriptName, - }), - }; -}; - -export const buildFmpUrl = ({ host, fileName, scriptName, parameter }) => { - const params = new URLSearchParams({ script: scriptName }); - if (parameter) { - params.set("param", parameter); - } - - return `fmp://${host}/${encodeURIComponent(fileName)}?${params.toString()}`; -}; diff --git a/packages/cli/template/vite-wv/scripts/upload.js b/packages/cli/template/vite-wv/scripts/upload.js deleted file mode 100644 index 71279450..00000000 --- a/packages/cli/template/vite-wv/scripts/upload.js +++ /dev/null @@ -1,27 +0,0 @@ -import { resolve } from "node:path"; - -import open from "open"; - -import packageJson from "../package.json" with { type: "json" }; -import { deployHtml } from "./filemaker.js"; - -const currentDirectory = import.meta.dirname; -const thePath = resolve(currentDirectory, "../dist", "index.html"); -const deployment = await deployHtml({ - appName: packageJson.name, - path: thePath, -}); - -if (deployment.method === "none") { - console.error( - "Could not resolve a FileMaker file. Start the local FM MCP proxy with a connected file, or set FM_SERVER and FM_DATABASE in .env.", - ); - process.exit(1); -} - -if (deployment.method === "bridge") { - console.log("Deployed via FM MCP bridge."); - process.exit(0); -} - -await open(deployment.url); diff --git a/packages/cli/template/vite-wv/src/app.tsx b/packages/cli/template/vite-wv/src/app.tsx deleted file mode 100644 index 896504eb..00000000 --- a/packages/cli/template/vite-wv/src/app.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { globalSettings } from "@proofkit/webviewer"; -import type { LucideIcon } from "lucide-react"; -import { Database, Layers, Sparkles } from "lucide-react"; - -type Step = { - readonly icon: LucideIcon; - readonly title: string; - readonly body: string; -}; - -globalSettings.setWebViewerName("web"); - -const steps: readonly Step[] = [ - { - icon: Database, - title: "Connect FileMaker later", - body: "This starter renders safely in a normal browser. When you are ready, wire in FM MCP or hosted FileMaker setup with ProofKit commands.", - }, - { - icon: Layers, - title: "Generate clients when ready", - body: "Add layouts to proofkit-typegen.config.jsonc, then run your typegen script to create strongly typed layout clients.", - }, - { - icon: Sparkles, - title: "Add shadcn components fast", - body: "Tailwind v4 and shadcn are already initialized, so agents and developers can add components without extra setup.", - }, -] as const; - -export default function App() { - return ( -
-
-
-
- - ProofKit Web Viewer Starter -
- -
-
-

- React + TypeScript + Vite -

-

- Build browser-safe FileMaker Web Viewer apps without scaffolding - against a hosted server. -

-

- This starter stays intentionally small, but it is already ready - for Tailwind v4, shadcn component installs, hash-based TanStack - Router navigation, React Query, and later ProofKit typegen - output. -

- -
- - pnpm dev - - - pnpm typegen - - - pnpm launch-fm - -
-
- - -
- -
- {steps.map((step) => ( -
- -

{step.title}

-

- {step.body} -

-
- ))} -
-
-
-
- ); -} diff --git a/packages/cli/template/vite-wv/src/index.css b/packages/cli/template/vite-wv/src/index.css deleted file mode 100644 index 0a7e6d7e..00000000 --- a/packages/cli/template/vite-wv/src/index.css +++ /dev/null @@ -1,91 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -:root { - --background: hsl(42 33% 98%); - --foreground: hsl(222 47% 11%); - --card: hsl(0 0% 100%); - --card-foreground: hsl(222 47% 11%); - --popover: hsl(0 0% 100%); - --popover-foreground: hsl(222 47% 11%); - --primary: hsl(197 82% 44%); - --primary-foreground: hsl(210 40% 98%); - --secondary: hsl(210 20% 93%); - --secondary-foreground: hsl(222 47% 11%); - --muted: hsl(42 21% 94%); - --muted-foreground: hsl(215 16% 40%); - --accent: hsl(32 88% 92%); - --accent-foreground: hsl(24 10% 10%); - --destructive: hsl(0 72% 51%); - --destructive-foreground: hsl(210 40% 98%); - --border: hsl(30 14% 86%); - --input: hsl(30 14% 86%); - --ring: hsl(197 82% 44%); - --radius: 1rem; -} - -.dark { - color-scheme: dark; - --background: hsl(221 39% 11%); - --foreground: hsl(44 23% 92%); - --card: hsl(222 33% 15%); - --card-foreground: hsl(44 23% 92%); - --popover: hsl(222 33% 15%); - --popover-foreground: hsl(44 23% 92%); - --primary: hsl(190 82% 62%); - --primary-foreground: hsl(222 47% 11%); - --secondary: hsl(219 19% 22%); - --secondary-foreground: hsl(44 23% 92%); - --muted: hsl(219 19% 22%); - --muted-foreground: hsl(215 20% 72%); - --accent: hsl(27 42% 28%); - --accent-foreground: hsl(44 23% 92%); - --destructive: hsl(0 63% 54%); - --destructive-foreground: hsl(210 40% 98%); - --border: hsl(219 19% 26%); - --input: hsl(219 19% 26%); - --ring: hsl(190 82% 62%); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); -} - -@layer base { - * { - @apply border-border; - } - - html { - color-scheme: light; - } - - body { - background-color: var(--background); - color: var(--foreground); - font-family: "Instrument Sans", Inter, ui-sans-serif, system-ui, sans-serif; - min-width: 320px; - } -} diff --git a/packages/cli/template/vite-wv/src/lib/utils.ts b/packages/cli/template/vite-wv/src/lib/utils.ts deleted file mode 100644 index 31f0864a..00000000 --- a/packages/cli/template/vite-wv/src/lib/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ClassValue } from "clsx"; -import { clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); diff --git a/packages/cli/template/vite-wv/src/main.tsx b/packages/cli/template/vite-wv/src/main.tsx deleted file mode 100644 index c6f0a6d1..00000000 --- a/packages/cli/template/vite-wv/src/main.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { RouterProvider } from "@tanstack/react-router"; -import React from "react"; -import ReactDOM from "react-dom/client"; - -import "./index.css"; -import { createAppRouter } from "./router"; - -const queryClient = new QueryClient(); -const routerPromise = createAppRouter(queryClient); - -const BootstrapPending = () => ( -
-
-
-); - -const AppRouter = () => { - const router = React.use(routerPromise); - - return ; -}; - -const rootElement = document.querySelector("#root"); -if (!rootElement) { - throw new Error("Root element with id 'root' not found"); -} - -ReactDOM.createRoot(rootElement).render( - - - }> - - - - , -); diff --git a/packages/cli/template/vite-wv/src/router.tsx b/packages/cli/template/vite-wv/src/router.tsx deleted file mode 100644 index 68b18048..00000000 --- a/packages/cli/template/vite-wv/src/router.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { fmFetch } from "@proofkit/webviewer"; -import type { QueryClient } from "@tanstack/react-query"; -import { - createHashHistory, - createRootRouteWithContext, - createRoute, - createRouter, - Link, - Outlet, -} from "@tanstack/react-router"; -import { z } from "zod/v4"; - -import App from "./app"; -import { QueryDemoPage } from "./routes/query-demo"; - -const RootLayout = () => ( -
-
- -
- -
-); - -// INITIAL PROPS PATTERN // -// If you want to use the inital props pattern, set the variable below with the name of your script that gets your app's initial props -const initialPropsSchema = z.object({ - initialRoute: z.string().optional(), -}); -const getInitialPropsScriptName = "" -//////////////////////////// - -type RouterContext = { - queryClient: QueryClient; - initialProps?: z.infer; -}; - -const rootRoute = createRootRouteWithContext()({ - component: RootLayout, -}); - -const indexRoute = createRoute({ - component: App, - getParentRoute: () => rootRoute, - path: "/", -}); - -const queryDemoRoute = createRoute({ - component: QueryDemoPage, - getParentRoute: () => rootRoute, - path: "/query", -}); - -const routeTree = rootRoute.addChildren([indexRoute, queryDemoRoute]); - -export const createAppRouter = async (queryClient: QueryClient) => { - let initialProps: z.infer | undefined; - - if (getInitialPropsScriptName) { - console.log("[router:init] fetching initial props"); - const result = await fmFetch(getInitialPropsScriptName, {}); - const parsedInitialProps = initialPropsSchema.safeParse(result); - if (!parsedInitialProps.success) { - console.error("[router:init] invalid initial props", { - error: parsedInitialProps.error, - result, - }); - throw parsedInitialProps.error; - } - initialProps = parsedInitialProps.data; - } - - const initialRoute = initialProps?.initialRoute; - if (initialRoute && !window.location.hash) { - console.log("[router:init] initial route", { - currentHash: window.location.hash, - initialRoute, - willSetHash: Boolean(initialRoute && !window.location.hash), - }); - window.location.hash = initialRoute; - } - - return createRouter({ - context: { queryClient, initialProps }, - history: createHashHistory(), - routeTree, - }); -}; - -declare module "@tanstack/react-router" { - interface Register { - router: Awaited>; - } -} diff --git a/packages/cli/template/vite-wv/src/routes/query-demo.tsx b/packages/cli/template/vite-wv/src/routes/query-demo.tsx deleted file mode 100644 index 75c5c93a..00000000 --- a/packages/cli/template/vite-wv/src/routes/query-demo.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { Link } from "@tanstack/react-router"; - -const getConnectionHint = (): string => - "Use fmFetch or generated clients once your FileMaker file is ready."; - -export const QueryDemoPage = () => { - const hintQuery = useQuery({ - queryFn: getConnectionHint, - queryKey: ["starter-connection-hint"] as const, - }); - - return ( -
-
-

- React Query ready -

-

- TanStack Query is preconfigured -

-

- This route is rendered by TanStack Router using hash history, which is - recommended for FileMaker Web Viewer apps. -

- -
- {hintQuery.isLoading ? "Loading starter data..." : hintQuery.data} -
- -
- - Back to starter - -
-
-
- ); -}; diff --git a/packages/cli/template/vite-wv/tsconfig.json b/packages/cli/template/vite-wv/tsconfig.json deleted file mode 100644 index 46fa82ef..00000000 --- a/packages/cli/template/vite-wv/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "noEmit": true, - "paths": { - "@/*": ["./src/*"], - }, - }, - "include": ["src"], -} diff --git a/packages/cli/template/vite-wv/vite.config.ts b/packages/cli/template/vite-wv/vite.config.ts deleted file mode 100644 index 670394a7..00000000 --- a/packages/cli/template/vite-wv/vite.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from "node:path"; - -import { fmBridge } from "@proofkit/webviewer/vite-plugins"; -import tailwindcss from "@tailwindcss/vite"; -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; -import { viteSingleFile } from "vite-plugin-singlefile"; - -const currentDirectory = import.meta.dirname; - -export default defineConfig({ - plugins: [fmBridge(), react(), tailwindcss(), viteSingleFile()], - resolve: { - alias: { - "@": path.resolve(currentDirectory, "./src"), - }, - }, - server: { - port: 5175, - }, -}); diff --git a/packages/cli/tests/browser-apps.smoke.test.ts b/packages/cli/tests/browser-apps.smoke.test.ts deleted file mode 100644 index b2ddb1b2..00000000 --- a/packages/cli/tests/browser-apps.smoke.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { z } from "zod/v4"; - -import { verifySmokeProjectBuilds } from "./test-utils.js"; - -const smokeEnvSchema = z.object({ - OTTO_SERVER_URL: z.url(), - OTTO_ADMIN_API_KEY: z.string().min(1), - FM_DATA_API_KEY: z.string().min(1), - FM_FILE_NAME: z.string().min(1), -}); - -const parsedSmokeEnv = smokeEnvSchema.safeParse(process.env); -const describeWhenSmokeEnvPresent = parsedSmokeEnv.success ? describe : describe.skip; - -if (!parsedSmokeEnv.success) { - const missingKeys = [...new Set(parsedSmokeEnv.error.issues.map((issue) => issue.path.join(".")))]; - console.warn(`Skipping external integration smoke tests; missing required env vars: ${missingKeys.join(", ")}`); -} - -describeWhenSmokeEnvPresent("External integration smoke tests (non-interactive CLI)", () => { - if (!parsedSmokeEnv.success) { - return; - } - - const testDir = join(tmpdir(), "proofkit-cli-tests"); - const cliPath = join(import.meta.dirname, "..", "bin", "proofkit.cjs"); - const projectName = "test-fm-project"; - const projectDir = join(testDir, projectName); - - // Required for live Otto/FileMaker integration smoke coverage. - const testEnv = parsedSmokeEnv.data; - - beforeEach( - () => { - // Clean up any stale test project from previous runs - if (existsSync(projectDir)) { - rmSync(projectDir, { recursive: true, force: true }); - } - // Ensure the test directory exists - mkdirSync(testDir, { recursive: true }); - }, - 30_000, // 30s timeout for cleanup of large node_modules - ); - - it("should create a browser project with FileMaker integration in non-interactive mode", () => { - // Build the command with all necessary flags for non-interactive mode - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type browser", - "--data-source filemaker", - `--server "${testEnv.OTTO_SERVER_URL}"`, - `--admin-api-key "${testEnv.OTTO_ADMIN_API_KEY}"`, - `--data-api-key "${testEnv.FM_DATA_API_KEY}"`, - `--file-name "${testEnv.FM_FILE_NAME}"`, - "--no-git", // Skip git initialization for testing - "--no-install", // Release smoke runs before the new CLI version is published. - ].join(" "); - - // Execute the command - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - // Verify project structure - expect(existsSync(projectDir)).toBe(true); - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(projectDir, ".env"))).toBe(true); - - // Verify package.json content - const pkgJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")); - expect(pkgJson.name).toBe(projectName); - - // Verify proofkit.json content - const proofkitConfig = JSON.parse(readFileSync(join(projectDir, "proofkit.json"), "utf-8")); - expect(proofkitConfig.appType).toBe("browser"); - expect(proofkitConfig.dataSources).toContainEqual( - expect.objectContaining({ - type: "fm", - name: "filemaker", - }), - ); - - // Verify the project can be built successfully - verifySmokeProjectBuilds(projectDir); - }); -}); diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts deleted file mode 100644 index fb94995b..00000000 --- a/packages/cli/tests/cli.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { execFileSync, spawnSync } from "node:child_process"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageDir = path.join(__dirname, ".."); -const distEntry = path.join(packageDir, "bin/proofkit.cjs"); -const semverOutputPattern = /^\d+\.\d+\.\d+(?:-[\w.]+)?\n$/; - -describe("proofkit CLI", () => { - it("prints raw version with -v", () => { - const output = execFileSync("node", [distEntry, "-v"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(output).toMatch(semverOutputPattern); - expect(output).not.toContain("_______"); - expect(output).not.toContain("ProofKit"); - }); - - it("shows kebab-case init flags in help", () => { - const output = execFileSync("node", [distEntry, "init", "--help"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(output).toContain("--app-type"); - expect(output).toContain("--proofkit-token"); - expect(output).toContain("--non-interactive"); - expect(output).toContain("--no-install"); - expect(output).toContain("--no-git"); - expect(output).not.toContain("--ui"); - expect(output).not.toContain("--appType"); - }); - - it("prints the header and project command guidance when run inside a ProofKit project", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-project-")); - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - - const output = execFileSync("node", [distEntry], { - cwd, - stdio: "pipe", - encoding: "utf8", - }); - - expect(output).toContain("_______"); - expect(output).toContain("Found"); - expect(output).toContain("Project commands"); - expect(output).toContain("proofkit doctor"); - expect(output).toContain("proofkit typegen"); - }); - - it("fails with guidance when no command is used in non-interactive mode", () => { - const result = spawnSync("node", [distEntry, "--non-interactive"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).not.toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("interactive-only in non-interactive mode"); - expect(`${result.stdout}\n${result.stderr}`).toContain("proofkit init --non-interactive"); - }); - - it("auto-detects piped execution as non-interactive", () => { - const result = spawnSync("node", [distEntry], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).not.toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("interactive-only in non-interactive mode"); - }); - - it("runs when invoked through a symlinked bin path", async () => { - const shimDir = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-shim-")); - const shimPath = path.join(shimDir, "proofkit"); - await fs.symlink(distEntry, shimPath); - - const result = spawnSync("node", [shimPath, "init", "--help"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).toBe(0); - expect(result.stdout).toContain("ProofKit"); - expect(result.stdout).toContain("Create a new project with ProofKit"); - expect(result.stdout).toContain("--app-type"); - expect(result.stdout).not.toContain("--ui"); - }); - - it("parses init proofkit token flag", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-token-")); - - const result = spawnSync( - "node", - [ - distEntry, - "init", - "token-app", - "--app-type", - "browser", - "--data-source", - "none", - "--non-interactive", - "--no-install", - "--no-git", - "--proofkit-token", - "parser-token", - ], - { - cwd, - stdio: "pipe", - encoding: "utf8", - env: { - ...process.env, - PROOFKIT_DISABLE_BUNDLED_BINARY: "1", - }, - }, - ); - - expect(result.status).toBe(0); - expect(await fs.pathExists(path.join(cwd, "token-app", "proofkit.json"))).toBe(true); - }); - - it("shows a clean invalid subcommand error by default", () => { - const result = spawnSync("node", [distEntry, "my-proofkit-app", "--force"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).not.toBe(0); - expect(output).toContain("Invalid subcommand for proofkit - use one of 'init', 'doctor', 'typegen'"); - expect(output).not.toContain('"CommandMismatch"'); - expect(output).not.toContain("[debug]"); - }); - - it("shows internal error details when debug mode is enabled", () => { - const result = spawnSync("node", [distEntry, "--debug", "my-proofkit-app", "--force"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).not.toBe(0); - expect(output).toContain("Invalid subcommand for proofkit - use one of 'init', 'doctor', 'typegen'"); - expect(output).toContain("[debug]"); - expect(output).toContain('"CommandMismatch"'); - }); -}); diff --git a/packages/cli/tests/default-command.test.ts b/packages/cli/tests/default-command.test.ts deleted file mode 100644 index d6ca6aa0..00000000 --- a/packages/cli/tests/default-command.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { NonInteractiveInputError, UserCancelledError } from "~/core/errors.js"; -import { runDefaultCommand } from "~/index.js"; -import { getFailure } from "./effect-test-utils.js"; -import { makeTestLayer } from "./test-layer.js"; - -function createConsoleTranscript() { - return { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; -} - -describe("default command routing", () => { - it("routes to init when no ProofKit project is present", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-init-")); - const consoleTranscript = createConsoleTranscript(); - - await Effect.runPromise( - runDefaultCommand().pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - console: consoleTranscript, - prompts: { - text: ["routed-app"], - select: ["browser", "none"], - }, - }), - ), - ); - - expect(await fs.pathExists(path.join(cwd, "routed-app", "proofkit.json"))).toBe(true); - expect(consoleTranscript.success.at(-1) ?? "").toContain("Created routed-app"); - expect(consoleTranscript.note.some((entry) => entry.title === "Coming soon")).toBe(false); - }); - - it("shows the project menu when a ProofKit project is present in interactive mode", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-project-")); - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - const consoleTranscript = createConsoleTranscript(); - const promptTranscript = { - text: [], - password: [], - select: [], - searchSelect: [], - multiSearchSelect: [], - confirm: [], - }; - - await Effect.runPromise( - runDefaultCommand().pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - console: consoleTranscript, - prompts: { - select: ["doctor"], - }, - promptTranscript, - }), - ), - ); - - expect(promptTranscript.select).toEqual([ - { - message: "What would you like to do?", - options: ["typegen", "doctor", "docs"], - }, - ]); - expect(consoleTranscript.note.some((entry) => entry.title === "Project commands")).toBe(false); - }); - - it("preserves user cancellation from the project menu prompt", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-project-cancel-")); - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - - expect( - await getFailure( - runDefaultCommand().pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - prompts: { - select: ["__cancel__"], - }, - }), - ), - ), - ).toMatchObject( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - }); - - it("shows explicit project command guidance when a ProofKit project is present in non-interactive mode", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-project-ci-")); - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - const consoleTranscript = createConsoleTranscript(); - - await Effect.runPromise( - runDefaultCommand({ nonInteractive: true }).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: true, - console: consoleTranscript, - }), - ), - ); - - expect(consoleTranscript.note).toEqual([ - { - title: "Project commands", - message: expect.stringContaining("Use an explicit command such as `proofkit doctor`"), - }, - ]); - }); - - it("fails in non-interactive mode without an explicit command", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-ci-")); - - expect( - await getFailure( - runDefaultCommand({ nonInteractive: true }).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: true, - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: - "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit init --non-interactive`.", - }), - ); - }); -}); diff --git a/packages/cli/tests/doctor.test.ts b/packages/cli/tests/doctor.test.ts deleted file mode 100644 index 0aa2fe71..00000000 --- a/packages/cli/tests/doctor.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { runDoctor } from "~/core/doctor.js"; -import { makeTestLayer } from "./test-layer.js"; - -function createConsoleTranscript() { - return { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; -} - -describe("doctor command", () => { - it("reports missing proofkit project", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-doctor-missing-")); - const consoleTranscript = createConsoleTranscript(); - - await Effect.runPromise( - runDoctor.pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - console: consoleTranscript, - }), - ), - ); - - expect(consoleTranscript.note[0]?.title).toBe("Doctor"); - expect(consoleTranscript.note[0]?.message).toContain("No ProofKit project found"); - expect(consoleTranscript.note[0]?.message).toContain("proofkit init"); - }); - - it("reports missing typegen config and package-native next steps", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-doctor-project-")); - const consoleTranscript = createConsoleTranscript(); - - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - await fs.writeJson(path.join(cwd, "package.json"), { - name: "doctor-test", - scripts: { - typegen: "npx @proofkit/typegen", - }, - devDependencies: { - "@proofkit/typegen": "workspace:*", - }, - }); - - await Effect.runPromise( - runDoctor.pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - console: consoleTranscript, - }), - ), - ); - - expect(consoleTranscript.note[0]?.title).toBe("Doctor"); - expect(consoleTranscript.note[0]?.message).toContain("Missing `proofkit-typegen.config.jsonc`"); - expect(consoleTranscript.note[0]?.message).toContain("npx @proofkit/typegen init"); - expect(consoleTranscript.note[0]?.message).toContain("npx @proofkit/typegen ui"); - }); -}); diff --git a/packages/cli/tests/effect-test-utils.ts b/packages/cli/tests/effect-test-utils.ts deleted file mode 100644 index c92f65e2..00000000 --- a/packages/cli/tests/effect-test-utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Effect as Fx } from "effect"; -import { Cause, Effect, Exit } from "effect"; -import { getOrUndefined } from "effect/Option"; - -export async function getFailure(effect: Fx.Effect) { - const exit = await Effect.runPromiseExit(effect); - if (!Exit.isFailure(exit)) { - throw new Error("Expected effect to fail."); - } - const failure = getOrUndefined(Cause.failureOption(exit.cause)); - if (!failure) { - throw new Error("Expected failure cause."); - } - return failure; -} diff --git a/packages/cli/tests/executor.test.ts b/packages/cli/tests/executor.test.ts deleted file mode 100644 index a86c1712..00000000 --- a/packages/cli/tests/executor.test.ts +++ /dev/null @@ -1,787 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { DirectoryConflictError, ExternalCommandError, UserCancelledError } from "~/core/errors.js"; -import { executeInitPlan } from "~/core/executeInitPlan.js"; -import { planInit } from "~/core/planInit.js"; -import { getFailure } from "./effect-test-utils.js"; -import { getSharedTemplateDir, makeInitRequest, readScaffoldArtifacts } from "./init-fixtures.js"; -import { makeTestLayer } from "./test-layer.js"; - -describe("executeInitPlan command paths", () => { - it("runs install, git, codegen, and filemaker bootstrap through services", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-exec-")); - const tracker = { - commands: [] as string[], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - }; - - const plan = planInit( - makeInitRequest({ - projectName: "fm-app", - scopedAppName: "fm-app", - appDir: "fm-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: false, - noGit: false, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "hosted-otto", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - server: "https://example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - packageManagerVersion: "11.0.0", - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm", tracker }))); - - expect(tracker.commands).toEqual([ - "pnpm install", - [ - "pnpx ultracite@^7 init --quiet --linter oxlint --pm pnpm --frameworks react --editors cursor", - "--agents claude codex --hooks cursor windsurf", - ].join(" "), - "pnpx @tanstack/intent@latest install", - "pnpm fix", - "pnpm lint", - ]); - expect(tracker.filemakerBootstraps).toBe(1); - expect(tracker.codegens).toBe(1); - expect(tracker.gitInits).toBe(1); - - const { proofkitJson, envFile, typegenConfig, pnpmWorkspaceFile } = await readScaffoldArtifacts( - path.join(cwd, "fm-app"), - ); - expect(proofkitJson.dataSources).toHaveLength(1); - expect(envFile).toContain("FM_DATABASE=Contacts.fmp12"); - expect(typegenConfig).toContain("API_Contacts"); - expect(typegenConfig).toContain("Contacts"); - expect(pnpmWorkspaceFile).toContain("trustPolicy: no-downgrade"); - expect(pnpmWorkspaceFile).toContain("trustPolicyIgnoreAfter: 43200"); - expect(pnpmWorkspaceFile).toContain("blockExoticSubdeps: true"); - }); - - it("writes browser Ultracite config without external init in no-install mode", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-ultracite-browser-")); - const tracker = { - commands: [] as string[], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - }; - - const plan = planInit( - makeInitRequest({ - appType: "browser", - dataSource: "none", - packageManager: "npm", - noInstall: true, - noGit: true, - cwd, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "npm", tracker }))); - - const { huskyPreCommitFile, npmrcFile } = await readScaffoldArtifacts(path.join(cwd, "demo-app")); - - expect(tracker.commands).toEqual([]); - expect(huskyPreCommitFile).toContain("pnpm exec lint-staged"); - expect(npmrcFile).toContain("min-release-age=1"); - }); - - it("warns and continues when final lint fails", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-lint-fix-warn-")); - const console = { - error: [] as string[], - info: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - success: [] as string[], - warn: [] as string[], - }; - const tracker = { - commands: [] as string[], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - }; - - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "none", - packageManager: "pnpm", - noInstall: false, - noGit: true, - cwd, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - packageManagerVersion: "11.0.0", - }, - ); - - await Effect.runPromise( - executeInitPlan(plan).pipe( - makeTestLayer({ - console, - cwd, - failProcessCommand: "pnpm lint", - failures: { - processRun: new ExternalCommandError({ - args: ["lint"], - command: "pnpm", - cwd, - message: "lint failed", - }), - }, - packageManager: "pnpm", - tracker, - }), - ), - ); - - expect(tracker.commands).toContain("pnpm fix"); - expect(tracker.commands).toContain("pnpm lint"); - expect(console.warn).toContain("Lint did not succeed; continuing setup."); - }); - - it("supports force overwrite for an existing directory", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-force-")); - const projectDir = path.join(cwd, "force-app"); - await fs.ensureDir(projectDir); - await fs.writeFile(path.join(projectDir, "README.md"), "old content"); - - const plan = planInit( - makeInitRequest({ - projectName: "force-app", - scopedAppName: "force-app", - appDir: "force-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: true, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm" }))); - - expect(await fs.pathExists(path.join(projectDir, "README.md"))).toBe(true); - expect(await fs.readFile(path.join(projectDir, "README.md"), "utf8")).not.toBe("old content"); - }); - - it("persists selected local MCP file into typegen config", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-local-mcp-explicit-")); - - const plan = planInit( - makeInitRequest({ - projectName: "local-mcp-app", - scopedAppName: "local-mcp-app", - appDir: "local-mcp-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "local-fm-mcp", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - fmMcpBaseUrl: "http://127.0.0.1:1365", - fileName: "Selected.fmp12", - }, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm" }))); - - const { typegenConfig } = await readScaffoldArtifacts(path.join(cwd, "local-mcp-app")); - expect(typegenConfig).toContain('"connectedFileName": "Selected.fmp12"'); - }); - - it("does not persist local MCP token into typegen config", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-local-mcp-token-config-")); - - const plan = planInit( - makeInitRequest({ - projectName: "local-mcp-token-app", - scopedAppName: "local-mcp-token-app", - appDir: "local-mcp-token-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - proofkitToken: "secret-session-token", - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "local-fm-mcp", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - fmMcpBaseUrl: "http://127.0.0.1:1365", - fileName: "Selected.fmp12", - proofkitToken: "secret-session-token", - }, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm" }))); - - const { typegenConfig } = await readScaffoldArtifacts(path.join(cwd, "local-mcp-token-app")); - expect(typegenConfig).not.toContain("secret-session-token"); - expect(typegenConfig).not.toContain("proofkitToken"); - }); - - it("persists the single auto-selected local MCP file into typegen config", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-local-mcp-single-")); - - const tracker = { - commands: [] as string[], - gitInits: 0, - codegens: 0, - codegenTokens: [] as Array, - filemakerBootstraps: 0, - }; - const plan = planInit( - makeInitRequest({ - projectName: "single-local-mcp-app", - scopedAppName: "single-local-mcp-app", - appDir: "single-local-mcp-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - proofkitToken: "initial-codegen-token", - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - fileMaker: { - mode: "local-fm-mcp", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - fmMcpBaseUrl: "http://127.0.0.1:1365", - fileName: "OnlyOpen.fmp12", - }, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - plan.tasks.runInitialCodegen = true; - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm", tracker }))); - - expect(tracker.codegenTokens).toEqual(["initial-codegen-token"]); - - const { typegenConfig } = await readScaffoldArtifacts(path.join(cwd, "single-local-mcp-app")); - expect(typegenConfig).toContain('"connectedFileName": "OnlyOpen.fmp12"'); - }); - - it("persists detected local MCP file into typegen config when webviewer setup skips filemaker bootstrap", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-local-mcp-skip-bootstrap-")); - - const plan = planInit( - makeInitRequest({ - projectName: "skip-bootstrap-local-mcp-app", - scopedAppName: "skip-bootstrap-local-mcp-app", - appDir: "skip-bootstrap-local-mcp-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - await Effect.runPromise( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - baseUrl: "http://127.0.0.1:1365", - connectedFiles: ["Autodetected.fmp12"], - }, - }, - }), - ), - ); - - const { typegenConfig } = await readScaffoldArtifacts(path.join(cwd, "skip-bootstrap-local-mcp-app")); - expect(typegenConfig).toContain('"connectedFileName": "Autodetected.fmp12"'); - }); - - it("fails with a typed directory conflict in non-interactive mode", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-conflict-")); - const projectDir = path.join(cwd, "conflict-app"); - await fs.ensureDir(projectDir); - await fs.writeFile(path.join(projectDir, "README.md"), "existing"); - - const plan = planInit( - makeInitRequest({ - projectName: "conflict-app", - scopedAppName: "conflict-app", - appDir: "conflict-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect(await getFailure(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm" })))).toMatchObject( - new DirectoryConflictError({ - message: - "conflict-app already exists and isn't empty. Remove the existing files or choose a different directory.", - path: projectDir, - }), - ); - }); - - it("fails with a typed cancelation error when overwrite is aborted", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-abort-")); - const projectDir = path.join(cwd, "abort-app"); - await fs.ensureDir(projectDir); - await fs.writeFile(path.join(projectDir, "README.md"), "existing"); - - const plan = planInit( - makeInitRequest({ - projectName: "abort-app", - scopedAppName: "abort-app", - appDir: "abort-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: false, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - prompts: { - select: ["abort"], - }, - }), - ), - ), - ).toMatchObject( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - }); - - it("fails with a typed directory conflict when overwrite prompt is cancelled", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-overwrite-cancel-")); - const projectDir = path.join(cwd, "cancel-app"); - await fs.ensureDir(projectDir); - await fs.writeFile(path.join(projectDir, "README.md"), "existing"); - - const plan = planInit( - makeInitRequest({ - projectName: "cancel-app", - scopedAppName: "cancel-app", - appDir: "cancel-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: false, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - prompts: { - select: ["__cancel_value__"], - }, - }), - ), - ), - ).toMatchObject( - new DirectoryConflictError({ - message: "Unable to choose how to handle the existing directory.", - path: projectDir, - }), - ); - }); - - it("fails with a typed directory conflict when clear confirmation is cancelled", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-clear-cancel-")); - const projectDir = path.join(cwd, "clear-cancel-app"); - const existingFile = path.join(projectDir, "README.md"); - await fs.ensureDir(projectDir); - await fs.writeFile(existingFile, "existing"); - - const plan = planInit( - makeInitRequest({ - projectName: "clear-cancel-app", - scopedAppName: "clear-cancel-app", - appDir: "clear-cancel-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: false, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - prompts: { - select: ["clear"], - confirm: ["__cancel_value__"], - }, - }), - ), - ), - ).toMatchObject( - new DirectoryConflictError({ - message: "Unable to confirm directory clearing.", - path: projectDir, - }), - ); - await expect(fs.pathExists(existingFile)).resolves.toBe(true); - }); - - it("prints pnpm warning in npm next steps", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-npm-warning-")); - const console = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const plan = planInit( - makeInitRequest({ - projectName: "npm-app", - scopedAppName: "npm-app", - appDir: "npm-app", - packageManager: "npm", - noInstall: true, - noGit: true, - cwd, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - packageManagerVersion: "10.0.0", - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "npm", console }))); - - expect(console.info.join("\n")).toContain( - "Warning: We strongly suggest using PNPM 11 or greater as your package manager to better protect your computer and your app.", - ); - }); - - it("prints package manager execute command in agent setup next steps", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-pnpm-agent-setup-")); - const console = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const plan = planInit( - makeInitRequest({ - projectName: "pnpm-app", - scopedAppName: "pnpm-app", - appDir: "pnpm-app", - packageManager: "pnpm", - noInstall: true, - noGit: true, - cwd, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - packageManagerVersion: "11.0.0", - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm", console }))); - - expect(console.info.join("\n")).not.toContain("pnpx @tanstack/intent@latest install"); - }); - - it("fails with a typed external command error when install fails", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-install-fail-")); - const consoleTranscript = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const plan = planInit( - makeInitRequest({ - projectName: "install-fail", - scopedAppName: "install-fail", - appDir: "install-fail", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "npm", - noInstall: false, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "npm", - console: consoleTranscript, - failures: { - processRun: new ExternalCommandError({ - message: "install failed", - command: "npm", - args: ["install"], - cwd, - }), - }, - }), - ), - ), - ).toMatchObject( - new ExternalCommandError({ - message: "install failed", - command: "npm", - args: ["install"], - cwd, - }), - ); - const installError = consoleTranscript.error.at(-1) ?? ""; - expect(installError).toContain("Install failed."); - expect(installError).toContain("Project root:"); - expect(installError).toContain("Failed command:"); - expect(installError).toContain("npm install"); - expect(installError).toContain("Succeeded before failure:"); - expect(installError).toContain("scaffold files"); - expect(installError).toContain("Continue troubleshooting:"); - expect(installError).toContain("cd install-fail"); - expect(installError).toContain("Start over:"); - }); - - it("fails with a typed codegen error when initial codegen fails", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-codegen-fail-")); - const plan = planInit( - makeInitRequest({ - projectName: "codegen-fail", - scopedAppName: "codegen-fail", - appDir: "codegen-fail", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - plan.tasks.runInitialCodegen = true; - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - failures: { - codegenRun: new ExternalCommandError({ - message: "Initial codegen failed", - command: "pnpm", - args: ["typegen"], - cwd: path.join(cwd, "codegen-fail"), - }), - }, - }), - ), - ), - ).toMatchObject( - new ExternalCommandError({ - message: "Initial codegen failed", - command: "pnpm", - args: ["typegen"], - cwd: path.join(cwd, "codegen-fail"), - }), - ); - }); -}); diff --git a/packages/cli/tests/init-fixtures.ts b/packages/cli/tests/init-fixtures.ts deleted file mode 100644 index e7ee6afd..00000000 --- a/packages/cli/tests/init-fixtures.ts +++ /dev/null @@ -1,65 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import fs from "fs-extra"; -import type { InitRequest } from "~/core/types.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export function makeInitRequest(overrides: Partial = {}): InitRequest { - return { - projectName: "demo-app", - scopedAppName: "demo-app", - appDir: "demo-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: false, - noGit: false, - force: false, - cwd: "/tmp/workspace", - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - ...overrides, - }; -} - -export function getSharedTemplateDir(templateName: "nextjs-shadcn" | "vite-wv") { - return path.resolve(__dirname, `../template/${templateName}`); -} - -export async function readScaffoldArtifacts(projectDir: string) { - const packageJson = await fs.readJson(path.join(projectDir, "package.json")); - const proofkitJson = await fs.readJson(path.join(projectDir, "proofkit.json")); - const envFile = await fs.readFile(path.join(projectDir, ".env"), "utf8"); - const typegenPath = path.join(projectDir, "proofkit-typegen.config.jsonc"); - const typegenConfig = (await fs.pathExists(typegenPath)) ? await fs.readFile(typegenPath, "utf8") : undefined; - const agentsPath = path.join(projectDir, "AGENTS.md"); - const claudePath = path.join(projectDir, "CLAUDE.md"); - const cursorIgnorePath = path.join(projectDir, ".cursorignore"); - const launchPath = path.join(projectDir, ".claude", "launch.json"); - const pnpmWorkspacePath = path.join(projectDir, "pnpm-workspace.yaml"); - const npmrcPath = path.join(projectDir, ".npmrc"); - const huskyPreCommitPath = path.join(projectDir, ".husky", "pre-commit"); - - return { - packageJson, - proofkitJson, - envFile, - typegenConfig, - agentsFile: (await fs.pathExists(agentsPath)) ? await fs.readFile(agentsPath, "utf8") : undefined, - claudeFile: (await fs.pathExists(claudePath)) ? await fs.readFile(claudePath, "utf8") : undefined, - cursorIgnoreFile: (await fs.pathExists(cursorIgnorePath)) ? await fs.readFile(cursorIgnorePath, "utf8") : undefined, - launchConfig: (await fs.pathExists(launchPath)) ? await fs.readFile(launchPath, "utf8") : undefined, - pnpmWorkspaceFile: (await fs.pathExists(pnpmWorkspacePath)) - ? await fs.readFile(pnpmWorkspacePath, "utf8") - : undefined, - npmrcFile: (await fs.pathExists(npmrcPath)) ? await fs.readFile(npmrcPath, "utf8") : undefined, - huskyPreCommitFile: (await fs.pathExists(huskyPreCommitPath)) - ? await fs.readFile(huskyPreCommitPath, "utf8") - : undefined, - }; -} diff --git a/packages/cli/tests/init-non-interactive-failures.test.ts b/packages/cli/tests/init-non-interactive-failures.test.ts deleted file mode 100644 index 6c52ceb0..00000000 --- a/packages/cli/tests/init-non-interactive-failures.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { TYPEGEN_VERSION } from "../src/package-versions.js"; - -type ExecFailure = Error & { - status?: number | null; - stdout?: string | Buffer; - stderr?: string | Buffer; -}; -const typegenCommandPattern = /\b(?:npm run|pnpm|yarn|bun)\s+typegen\b/; - -function toText(value: string | Buffer | undefined) { - if (typeof value === "string") { - return value; - } - if (!value) { - return ""; - } - return value.toString("utf-8"); -} - -describe("Init Non-Interactive Failure Paths", () => { - const testDir = join(import.meta.dirname, "..", "..", "tmp", "init-failure-tests"); - const cliPath = join(import.meta.dirname, "..", "bin", "proofkit.cjs"); - const expectedTypegenVersion = `^${TYPEGEN_VERSION}`; - - beforeEach(() => { - rmSync(testDir, { recursive: true, force: true }); - mkdirSync(testDir, { recursive: true }); - }); - - const runInitCommand = (args: string[], cwd = testDir) => { - return execFileSync("node", [cliPath, "init", ...args], { - cwd, - env: process.env, - stdio: "pipe", - encoding: "utf-8", - }); - }; - - const runInitExpectFailure = (args: string[], cwd = testDir) => { - try { - runInitCommand(args, cwd); - throw new Error(`Expected init to fail, but it succeeded: ${args.join(" ")}`); - } catch (error) { - const failure = error as ExecFailure; - if (typeof failure.status === "number" || failure.status === null) { - return { - status: failure.status, - stdout: toText(failure.stdout), - stderr: toText(failure.stderr), - }; - } - throw error; - } - }; - - const runInitExpectSuccess = (args: string[], cwd = testDir) => runInitCommand(args, cwd); - - it("fails in non-interactive mode without a project name and does not scaffold", () => { - writeFileSync(join(testDir, "sentinel.txt"), "keep"); - - const result = runInitExpectFailure(["--non-interactive", "--app-type", "webviewer", "--no-install", "--no-git"]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Project name is required in non-interactive mode."); - expect(readdirSync(testDir).sort()).toEqual(["sentinel.txt"]); - }); - - it("normalizes spaces in non-interactive app names and creates the project directory", () => { - const projectName = "Bad Name"; - - runInitExpectSuccess([projectName, "--non-interactive", "--app-type", "webviewer", "--no-install", "--no-git"]); - - expect(existsSync(join(testDir, "bad-name"))).toBe(true); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("fails for invalid scoped-path edge cases before mutating the target directory", () => { - writeFileSync(join(testDir, "README.md"), "existing content"); - - const result = runInitExpectFailure([ - "@scope", - "--non-interactive", - "--app-type", - "webviewer", - "--no-install", - "--no-git", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Name must consist of only lowercase alphanumeric characters, '-', and '_'"); - expect(readFileSync(join(testDir, "README.md"), "utf-8")).toBe("existing content"); - expect(existsSync(join(testDir, "package.json"))).toBe(false); - expect(existsSync(join(testDir, "proofkit.json"))).toBe(false); - }); - - it("fails for partial FileMaker schema flags without creating a scaffold", () => { - const projectName = "partial-filemaker-flags"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--app-type", - "webviewer", - "--data-source", - "filemaker", - "--layout-name", - "Contacts", - "--no-install", - "--no-git", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Both --layout-name and --schema-name must be provided together."); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("fails when FileMaker flags are passed without selecting the filemaker data source", () => { - const projectName = "unsupported-filemaker-flags"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--app-type", - "webviewer", - "--layout-name", - "Contacts", - "--schema-name", - "Contacts", - "--no-install", - "--no-git", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("FileMaker flags require --data-source filemaker in non-interactive mode."); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("preserves existing directory contents when validation fails even with --force", () => { - const projectName = "force-validation-failure"; - const projectDir = join(testDir, projectName); - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, "README.md"), "existing content"); - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--app-type", - "webviewer", - "--force", - "--layout-name", - "Contacts", - "--schema-name", - "Contacts", - "--no-install", - "--no-git", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("FileMaker flags require --data-source filemaker in non-interactive mode."); - expect(readFileSync(join(projectDir, "README.md"), "utf-8")).toBe("existing content"); - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(false); - }); - - it("adds package-native typegen guidance for browser scaffolds", () => { - const projectName = "browser-no-fm-guidance"; - const output = runInitExpectSuccess([ - projectName, - "--non-interactive", - "--app-type", - "browser", - "--data-source", - "none", - "--no-install", - "--no-git", - ]); - - const packageJson = JSON.parse(readFileSync(join(testDir, projectName, "package.json"), "utf-8")) as { - scripts?: Record; - devDependencies?: Record; - }; - expect(packageJson.scripts?.typegen).toBe("typegen"); - expect(packageJson.devDependencies?.["@proofkit/typegen"]).toBe(expectedTypegenVersion); - expect(output).not.toMatch(typegenCommandPattern); - }); -}); diff --git a/packages/cli/tests/init-scaffold-contract.test.ts b/packages/cli/tests/init-scaffold-contract.test.ts deleted file mode 100644 index 34f394bd..00000000 --- a/packages/cli/tests/init-scaffold-contract.test.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { parse as parseJsonc } from "jsonc-parser/lib/esm/main.js"; -import { beforeEach, describe, expect, it } from "vitest"; -import { NODE_RUNTIME_VERSION } from "~/consts.js"; -import { FMDAPI_VERSION, TYPEGEN_VERSION, WEBVIEWER_VERSION } from "../src/package-versions.js"; - -interface PackageJsonShape { - version?: string; - name?: string; - packageManager?: string; - devEngines?: { - packageManager?: { - name?: string; - version?: string; - onFail?: string; - }; - runtime?: { - name?: string; - version?: string; - onFail?: string; - }; - }; - engines?: { - node?: string; - }; - scripts?: Record; - "lint-staged"?: Record; - dependencies?: Record; - devDependencies?: Record; - proofkitMetadata?: { - initVersion?: string; - }; -} - -interface ProofkitSettings { - appType?: string; - ui?: string; - envFile?: string; - dataSources?: unknown[]; -} - -const cliPath = join(import.meta.dirname, "..", "bin", "proofkit.cjs"); -const testDir = join(import.meta.dirname, "..", "..", "tmp", "cli-contract-tests"); -const browserProjectName = "contract-browser-project"; -const webviewerProjectName = "contract-webviewer-project"; -const browserProjectDir = join(testDir, browserProjectName); -const webviewerProjectDir = join(testDir, webviewerProjectName); -const cliPackageJsonPath = join(import.meta.dirname, "..", "package.json"); -const cliPackageJson = readJsonFile(cliPackageJsonPath); -const cliVersion = cliPackageJson.version ?? ""; -const expectedProofkitVersions = new Map([ - ["@proofkit/fmdapi", `^${FMDAPI_VERSION}`], - ["@proofkit/typegen", `^${TYPEGEN_VERSION}`], - ["@proofkit/webviewer", `^${WEBVIEWER_VERSION}`], -]); -const packageManagerVersionPattern = /^\^\d+\.\d+\.\d+/; -const ansiStylePrefixPattern = /^[0-9;]*m/; - -function runInit({ appType, projectName }: { appType: "browser" | "webviewer"; projectName: string }): string { - return execFileSync( - "node", - [ - cliPath, - "init", - projectName, - "--non-interactive", - "--app-type", - appType, - "--data-source", - "none", - "--no-git", - "--no-install", - ], - { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }, - ); -} - -function readJsonFile(filePath: string): T { - return JSON.parse(readFileSync(filePath, "utf-8")) as T; -} - -function getProofkitDependencyVersions(pkg: PackageJsonShape): [string, string][] { - const combined = { - ...(pkg.dependencies ?? {}), - ...(pkg.devDependencies ?? {}), - }; - - return Object.entries(combined) - .filter(([name]) => name.startsWith("@proofkit/")) - .map(([name, version]) => [name, version]); -} - -function allProofkitDependenciesUseCurrentVersions(pkg: PackageJsonShape): boolean { - const versions = getProofkitDependencyVersions(pkg); - return versions.length > 0 && versions.every(([name, version]) => version === expectedProofkitVersions.get(name)); -} - -function checkNodeSyntax(projectDir: string, relativeFilePath: string): boolean { - try { - execFileSync("node", ["--check", relativeFilePath], { - cwd: projectDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - - return true; - } catch { - return false; - } -} - -function getPackageManagerName(packageJson: PackageJsonShape): "npm" | "pnpm" | "yarn" | "bun" { - const raw = packageJson.devEngines?.packageManager?.name ?? packageJson.packageManager?.split("@")[0]; - if (raw === "pnpm" || raw === "yarn" || raw === "bun") { - return raw; - } - return "npm"; -} - -function formatRunCommand(pkgManager: "npm" | "pnpm" | "yarn" | "bun", command: string): string { - return pkgManager === "npm" || pkgManager === "bun" ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`; -} - -function sanitizeOutput(output: string): string { - return output - .split("\u001b[") - .map((segment, index) => (index === 0 ? segment : segment.replace(ansiStylePrefixPattern, ""))) - .join(""); -} - -function outputSuggestsCommand(output: string, command: string): boolean { - return output.includes(` ${command}`); -} - -describe("Init scaffold contract tests", () => { - beforeEach(() => { - rmSync(testDir, { recursive: true, force: true }); - mkdirSync(testDir, { recursive: true }); - }); - - it("creates deterministic browser scaffold output in non-interactive mode", () => { - const initOutput = runInit({ - appType: "browser", - projectName: browserProjectName, - }); - const normalizedOutput = sanitizeOutput(initOutput); - - expect(existsSync(browserProjectDir)).toBe(true); - expect(existsSync(join(browserProjectDir, "package.json"))).toBe(true); - expect(existsSync(join(browserProjectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(browserProjectDir, ".env"))).toBe(true); - expect(existsSync(join(browserProjectDir, ".cursorignore"))).toBe(true); - expect(existsSync(join(browserProjectDir, "pnpm-workspace.yaml"))).toBe(true); - expect(existsSync(join(browserProjectDir, "src", "lib", "env.ts"))).toBe(true); - expect(existsSync(join(browserProjectDir, "src", "app", "layout.tsx"))).toBe(true); - expect(existsSync(join(browserProjectDir, "postcss.config.mjs"))).toBe(true); - - const packageJson = readJsonFile(join(browserProjectDir, "package.json")); - expect(packageJson.name).toBe(browserProjectName); - expect(packageJson.scripts?.dev).toBe("next dev --turbopack"); - expect(packageJson.scripts?.build).toBe("next build --turbopack"); - expect(packageJson.scripts?.proofkit).toBeUndefined(); - expect(packageJson.proofkitMetadata?.initVersion).toBe(cliVersion); - expect(packageJson.packageManager).toBeUndefined(); - expect(packageJson.devEngines?.packageManager?.name).toBe("pnpm"); - expect(packageJson.devEngines?.packageManager?.version).toMatch(packageManagerVersionPattern); - expect(packageJson.devEngines?.packageManager?.onFail).toBe("download"); - expect(packageJson.devEngines?.runtime).toEqual({ - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }); - expect(packageJson.engines?.node).toBe(NODE_RUNTIME_VERSION); - expect(allProofkitDependenciesUseCurrentVersions(packageJson)).toBe(true); - expect(readFileSync(join(browserProjectDir, "CLAUDE.md"), "utf-8")).toBe("@AGENTS.md\n"); - expect(readFileSync(join(browserProjectDir, ".cursorignore"), "utf-8")).toBe("CLAUDE.md\n"); - expect(readFileSync(join(browserProjectDir, ".gitignore"), "utf-8")).toContain(".pnpm-store"); - expect(readFileSync(join(browserProjectDir, ".husky", "pre-commit"), "utf-8")).toBe( - '#!/bin/sh\necho "Running lint-staged..."\npnpm exec lint-staged\n', - ); - const pnpmWorkspaceText = readFileSync(join(browserProjectDir, "pnpm-workspace.yaml"), "utf-8"); - expect(pnpmWorkspaceText).toContain(' "esbuild": true'); - expect(pnpmWorkspaceText).toContain(' "msw": true'); - expect(pnpmWorkspaceText).toContain(' "@parcel/watcher": true'); - expect(pnpmWorkspaceText).toContain(' "node": true'); - expect(pnpmWorkspaceText).toContain(' "sharp": true'); - expect(pnpmWorkspaceText).toContain(' "msgpackr-extract": true'); - expect(pnpmWorkspaceText).toContain("packages:"); - expect(pnpmWorkspaceText).toContain(' - "."'); - expect(pnpmWorkspaceText).toContain("trustPolicy: no-downgrade"); - expect(pnpmWorkspaceText).toContain("trustPolicyIgnoreAfter: 43200"); - expect(pnpmWorkspaceText).toContain("blockExoticSubdeps: true"); - const pkgManager = getPackageManagerName(packageJson); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "typegen"))).toBe(false); - - const proofkitConfig = readJsonFile(join(browserProjectDir, "proofkit.json")); - expect(proofkitConfig.appType).toBe("browser"); - expect(proofkitConfig.ui).toBe("shadcn"); - expect(proofkitConfig.envFile).toBe(".env"); - expect(proofkitConfig.dataSources).toEqual([]); - - // Compile-equivalent smoke check without external installs. - expect(checkNodeSyntax(browserProjectDir, "postcss.config.mjs")).toBe(true); - }); - - it("creates deterministic webviewer scaffold output in non-interactive mode", () => { - const initOutput = runInit({ - appType: "webviewer", - projectName: webviewerProjectName, - }); - const normalizedOutput = sanitizeOutput(initOutput); - - expect(existsSync(webviewerProjectDir)).toBe(true); - expect(existsSync(join(webviewerProjectDir, "package.json"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "proofkit-typegen.config.jsonc"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, ".env"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, ".cursorignore"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "pnpm-workspace.yaml"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "src", "app.tsx"))).toBe(true); - expect(readdirSync(join(webviewerProjectDir, "src"))).not.toContain("App.tsx"); - expect(existsSync(join(webviewerProjectDir, "src", "main.tsx"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "scripts", "upload.js"))).toBe(true); - - const packageJson = readJsonFile(join(webviewerProjectDir, "package.json")); - expect(packageJson.name).toBe(webviewerProjectName); - expect(packageJson.scripts?.build).toBe("vite build"); - expect(packageJson.scripts?.typegen).toBe("pnpx @proofkit/typegen"); - expect(packageJson.scripts?.["typegen:ui"]).toBe("pnpx @proofkit/typegen ui"); - expect(packageJson.scripts?.check).toBe("ultracite check"); - expect(packageJson.scripts?.fix).toBe("ultracite fix"); - expect(packageJson.scripts?.proofkit).toBeUndefined(); - expect(packageJson["lint-staged"]).toEqual({ - "*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}": ["pnpm exec ultracite fix"], - }); - expect(packageJson.proofkitMetadata?.initVersion).toBe(cliVersion); - expect(packageJson.packageManager).toBeUndefined(); - expect(packageJson.devEngines?.packageManager?.name).toBe("pnpm"); - expect(packageJson.devEngines?.packageManager?.version).toMatch(packageManagerVersionPattern); - expect(packageJson.devEngines?.packageManager?.onFail).toBe("download"); - expect(packageJson.devEngines?.runtime).toEqual({ - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }); - expect(packageJson.engines?.node).toBe(NODE_RUNTIME_VERSION); - expect(allProofkitDependenciesUseCurrentVersions(packageJson)).toBe(true); - expect(readFileSync(join(webviewerProjectDir, "CLAUDE.md"), "utf-8")).toBe("@AGENTS.md\n"); - expect(readFileSync(join(webviewerProjectDir, ".cursorignore"), "utf-8")).toBe("CLAUDE.md\n"); - expect(readFileSync(join(webviewerProjectDir, ".gitignore"), "utf-8")).toContain(".pnpm-store"); - expect(readFileSync(join(webviewerProjectDir, ".husky", "pre-commit"), "utf-8")).toBe( - '#!/bin/sh\necho "Running lint-staged..."\npnpm exec lint-staged\n', - ); - const pkgManager = getPackageManagerName(packageJson); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "typegen"))).toBe(true); - - const proofkitConfig = readJsonFile(join(webviewerProjectDir, "proofkit.json")); - expect(proofkitConfig.appType).toBe("webviewer"); - expect(proofkitConfig.ui).toBe("shadcn"); - expect(proofkitConfig.envFile).toBe(".env"); - expect(proofkitConfig.dataSources).toEqual([]); - - const typegenConfigText = readFileSync(join(webviewerProjectDir, "proofkit-typegen.config.jsonc"), "utf-8"); - const typegenConfig = parseJsonc(typegenConfigText) as { - config?: { - type?: string; - path?: string; - validator?: string; - webviewerScriptName?: string; - fmMcp?: { - enabled?: boolean; - }; - }; - }; - expect(typegenConfig.config?.type).toBe("fmdapi"); - expect(typegenConfig.config?.path).toBe("./src/config/schemas/filemaker"); - expect(typegenConfig.config?.validator).toBe("zod/v4"); - expect(typegenConfig.config?.webviewerScriptName).toBe("ExecuteDataApi"); - expect(typegenConfig.config?.fmMcp?.enabled).toBe(true); - - const uploadScriptText = readFileSync(join(webviewerProjectDir, "scripts", "upload.js"), "utf-8"); - const filemakerHelperText = readFileSync(join(webviewerProjectDir, "scripts", "filemaker.js"), "utf-8"); - const pnpmWorkspaceText = readFileSync(join(webviewerProjectDir, "pnpm-workspace.yaml"), "utf-8"); - expect(uploadScriptText).toContain("const deployment = await deployHtml({"); - expect(uploadScriptText).toContain("Deployed via FM MCP bridge."); - expect(filemakerHelperText).toContain('scriptName = "deploy_html"'); - expect(pnpmWorkspaceText).toContain(' "esbuild": true'); - expect(pnpmWorkspaceText).toContain(' "msw": true'); - expect(pnpmWorkspaceText).toContain(' "@parcel/watcher": true'); - expect(pnpmWorkspaceText).toContain(' "sharp": false'); - expect(pnpmWorkspaceText).toContain(' "msgpackr-extract": true'); - expect(pnpmWorkspaceText).toContain("packages:"); - expect(pnpmWorkspaceText).toContain(' - "."'); - expect(pnpmWorkspaceText).toContain("trustPolicy: no-downgrade"); - expect(pnpmWorkspaceText).toContain("trustPolicyIgnoreAfter: 43200"); - expect(pnpmWorkspaceText).toContain("blockExoticSubdeps: true"); - - // Compile-equivalent smoke checks without external installs. - expect(checkNodeSyntax(webviewerProjectDir, "scripts/upload.js")).toBe(true); - }); -}); diff --git a/packages/cli/tests/install-fm-addon.test.ts b/packages/cli/tests/install-fm-addon.test.ts deleted file mode 100644 index f4ad2c85..00000000 --- a/packages/cli/tests/install-fm-addon.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { compareAddonVersions, getFmAddonInstallInstructions, inspectFmAddon } from "~/installers/install-fm-addon.js"; -import { getWebViewerAddonMessages } from "~/installers/proofkit-webviewer.js"; - -async function writeAddonVersion(dir: string, version: string) { - await fs.ensureDir(dir); - await fs.writeFile( - path.join(dir, "template.xml"), - ``, - "utf8", - ); -} - -describe("inspectFmAddon", () => { - it("returns unknown when the platform is unsupported", async () => { - const result = await inspectFmAddon( - { addonName: "wv" }, - { - targetDir: null, - latestAddonPath: "/tmp/latest-addon", - }, - ); - - expect(result.status).toBe("unknown"); - expect(result.reason).toBe("unsupported-platform"); - }); - - it("returns missing when the local add-on is absent", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-addon-missing-")); - const latestAddonPath = path.join(root, "latest", "ProofKitWV"); - const targetDir = path.join(root, "target"); - await writeAddonVersion(latestAddonPath, "2.2.3.0"); - await fs.ensureDir(targetDir); - - const result = await inspectFmAddon({ addonName: "wv" }, { targetDir, latestAddonPath }); - - expect(result.status).toBe("missing"); - expect(result.latestVersion).toBe("2.2.3.0"); - }); - - it("returns installed-current when versions match", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-addon-current-")); - const latestAddonPath = path.join(root, "latest", "ProofKitWV"); - const targetDir = path.join(root, "target"); - await writeAddonVersion(latestAddonPath, "2.2.3.0"); - await writeAddonVersion(path.join(targetDir, "ProofKitWV"), "2.2.3.0"); - - const result = await inspectFmAddon({ addonName: "wv" }, { targetDir, latestAddonPath }); - - expect(result.status).toBe("installed-current"); - expect(result.installedVersion).toBe("2.2.3.0"); - }); - - it("returns installed-outdated when the remote add-on is newer", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-addon-outdated-")); - const latestAddonPath = path.join(root, "latest", "ProofKitWV"); - const targetDir = path.join(root, "target"); - await writeAddonVersion(latestAddonPath, "2.2.4.0"); - await writeAddonVersion(path.join(targetDir, "ProofKitWV"), "2.2.3.0"); - - const result = await inspectFmAddon({ addonName: "wv" }, { targetDir, latestAddonPath }); - - expect(result.status).toBe("installed-outdated"); - expect(result.installedVersion).toBe("2.2.3.0"); - expect(result.latestVersion).toBe("2.2.4.0"); - }); - - it("returns unknown when installed metadata cannot be parsed", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-addon-unknown-")); - const latestAddonPath = path.join(root, "latest", "ProofKitWV"); - const targetDir = path.join(root, "target"); - await writeAddonVersion(latestAddonPath, "2.2.4.0"); - await fs.ensureDir(path.join(targetDir, "ProofKitWV")); - await fs.writeFile(path.join(targetDir, "ProofKitWV", "template.xml"), "", "utf8"); - - const result = await inspectFmAddon({ addonName: "wv" }, { targetDir, latestAddonPath }); - - expect(result.status).toBe("unknown"); - expect(result.reason).toBe("unreadable-version"); - }); -}); - -describe("compareAddonVersions", () => { - it("preserves the fourth version segment", () => { - expect(compareAddonVersions("2.2.3.0", "2.2.3.1")).toBe(-1); - expect(compareAddonVersions("2.2.3.1", "2.2.3.0")).toBe(1); - }); -}); - -describe("getWebViewerAddonMessages", () => { - it("points to the docs when the local add-on is outdated", () => { - const messages = getWebViewerAddonMessages({ - hasRequiredLayouts: true, - inspection: { - status: "installed-outdated", - addonName: "wv", - addonDir: "ProofKitWV", - addonDisplayName: "ProofKit Web Viewer", - targetDir: "/tmp/ProofKit", - installedPath: "/tmp/ProofKit/ProofKit.fmaddon", - installedVersion: "2.2.3.0", - remoteAssetUrl: "https://downloads.ottomatic.cloud/proofkit/addons/ProofKitWV.fmaddon", - latestVersion: "2.2.4.0", - }, - }); - - expect(messages.warn.join("\n")).toContain("https://proofkit.proof.sh/docs/webviewer"); - expect(messages.nextSteps).toEqual([ - "Update the ProofKit Web Viewer add-on: https://proofkit.proof.sh/docs/webviewer", - ]); - expect(messages.warn.join("\n")).not.toContain("proofkit add addon"); - }); -}); - -describe("getFmAddonInstallInstructions", () => { - it("includes direct-open install guidance", () => { - const instructions = getFmAddonInstallInstructions("wv"); - - expect(instructions.steps).toContain("When FileMaker opens the add-on file, confirm the install prompt"); - expect(instructions.steps.join("\n")).not.toContain("Restart FileMaker"); - }); -}); diff --git a/packages/cli/tests/integration.test.ts b/packages/cli/tests/integration.test.ts deleted file mode 100644 index 36df5e88..00000000 --- a/packages/cli/tests/integration.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { NODE_RUNTIME_VERSION } from "~/consts.js"; -import { executeInitPlan } from "~/core/executeInitPlan.js"; -import { planInit } from "~/core/planInit.js"; -import { getProofkitDependencyVersion, getTypegenVersion } from "~/utils/getProofKitVersion.js"; -import { detectUserPackageManager } from "~/utils/packageManager.js"; -import { getSharedTemplateDir, makeInitRequest, readScaffoldArtifacts } from "./init-fixtures.js"; -import { makeTestLayer } from "./test-layer.js"; - -const proofkitTypegenVersion = getProofkitDependencyVersion(getTypegenVersion()); - -describe("integration scaffold generation", () => { - it("creates a browser scaffold with proofkit.json and env", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-browser-")); - const projectDir = path.join(cwd, "browser-app"); - const consoleTranscript = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const layer = makeTestLayer({ - cwd, - packageManager: detectUserPackageManager(), - console: consoleTranscript, - }); - - const plan = planInit( - makeInitRequest({ - projectName: "browser-app", - scopedAppName: "browser-app", - appDir: "browser-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - packageManagerVersion: "11.1.0", - }, - ); - - await Effect.runPromise(layer(executeInitPlan(plan))); - - expect(await fs.pathExists(projectDir)).toBe(true); - expect(await fs.pathExists(path.join(projectDir, "package.json"))).toBe(true); - expect(await fs.pathExists(path.join(projectDir, "proofkit.json"))).toBe(true); - expect(await fs.pathExists(path.join(projectDir, ".env"))).toBe(true); - - const { packageJson, proofkitJson, envFile, claudeFile, cursorIgnoreFile } = - await readScaffoldArtifacts(projectDir); - - expect(packageJson.name).toBe("browser-app"); - expect(packageJson.packageManager).toBeUndefined(); - expect(packageJson.devEngines?.packageManager).toEqual({ - name: "pnpm", - version: "^11.1.0", - onFail: "download", - }); - expect(packageJson.devEngines?.runtime).toEqual({ - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }); - expect(packageJson.engines).toEqual({ - node: NODE_RUNTIME_VERSION, - }); - expect(packageJson.proofkitMetadata).toMatchObject({ - scaffoldPackage: "@proofkit/cli", - }); - expect(packageJson.devDependencies["@proofkit/cli"]).toBeUndefined(); - expect(packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - expect(packageJson.scripts.proofkit).toBeUndefined(); - expect(packageJson.scripts.typegen).toBe("typegen"); - expect(packageJson.scripts["typegen:ui"]).toBe("typegen ui"); - expect(typeof packageJson.proofkitMetadata?.initVersion).toBe("string"); - expect(packageJson.proofkitMetadata?.initVersion).not.toBe(""); - expect(proofkitJson).toMatchObject({ - appType: "browser", - dataSources: [], - envFile: ".env", - }); - expect(claudeFile).toBe("@AGENTS.md\n"); - expect(cursorIgnoreFile).toBe("CLAUDE.md\n"); - expect(envFile).toContain("# When adding additional environment variables"); - expect(consoleTranscript.success.at(-1) ?? "").toContain("Created browser-app"); - }); - - it("creates a webviewer scaffold without leaking state across runs", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-webviewer-")); - const firstDir = path.join(cwd, "first"); - const secondDir = path.join(cwd, "second"); - const layer = makeTestLayer({ - cwd, - packageManager: "pnpm", - }); - - const firstPlan = planInit( - makeInitRequest({ - projectName: "first", - scopedAppName: "first", - appDir: "first", - appType: "webviewer", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - const secondPlan = planInit( - makeInitRequest({ - projectName: "second", - scopedAppName: "second", - appDir: "second", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await Effect.runPromise(layer(executeInitPlan(firstPlan))); - await Effect.runPromise(layer(executeInitPlan(secondPlan))); - - const firstSettings = await fs.readJson(path.join(firstDir, "proofkit.json")); - const secondSettings = await fs.readJson(path.join(secondDir, "proofkit.json")); - expect(firstSettings.appType).toBe("webviewer"); - expect(secondSettings.appType).toBe("browser"); - }); - - it("creates a webviewer scaffold with ultracite, tanstack wiring, and agent files", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-webviewer-template-")); - const projectDir = path.join(cwd, "webviewer-app"); - const consoleTranscript = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const layer = makeTestLayer({ - cwd, - packageManager: "pnpm", - console: consoleTranscript, - }); - - const plan = planInit( - makeInitRequest({ - projectName: "webviewer-app", - scopedAppName: "webviewer-app", - appDir: "webviewer-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - await Effect.runPromise(layer(executeInitPlan(plan))); - - const { packageJson, agentsFile, claudeFile, cursorIgnoreFile, huskyPreCommitFile, launchConfig } = - await readScaffoldArtifacts(projectDir); - const routerFile = await fs.readFile(path.join(projectDir, "src/router.tsx"), "utf8"); - const mainFile = await fs.readFile(path.join(projectDir, "src/main.tsx"), "utf8"); - const queryDemoFile = await fs.readFile(path.join(projectDir, "src/routes/query-demo.tsx"), "utf8"); - const oxlintConfig = await fs.readFile(path.join(projectDir, "oxlint.config.ts"), "utf8"); - - expect(packageJson.scripts.lint).toBe("ultracite check ."); - expect(packageJson.scripts.format).toBe("ultracite fix ."); - expect(packageJson.scripts.check).toBe("ultracite check"); - expect(packageJson.scripts.fix).toBe("ultracite fix"); - expect(packageJson.scripts.proofkit).toBeUndefined(); - expect(packageJson.dependencies["@tanstack/react-query"]).toBe("^5.90.21"); - expect(packageJson.dependencies["@tanstack/react-router"]).toBe("^1.167.4"); - expect(packageJson.devDependencies.ultracite).toBe("^7.0.0"); - expect(agentsFile).toContain("Use the ProofKit docs as the primary reference"); - expect(claudeFile).toBe("@AGENTS.md\n"); - expect(cursorIgnoreFile).toBe("CLAUDE.md\n"); - expect(launchConfig).toContain('"runtimeExecutable": "pnpm"'); - expect(routerFile).toContain("createHashHistory"); - expect(mainFile).toContain("QueryClientProvider"); - expect(queryDemoFile).toContain("TanStack Query is preconfigured"); - expect(oxlintConfig).toContain('import { defineConfig } from "oxlint"'); - expect(oxlintConfig).toContain('import core from "ultracite/oxlint/core"'); - expect(oxlintConfig).toContain('import react from "ultracite/oxlint/react"'); - expect(oxlintConfig).toContain('"react/react-in-jsx-scope": "off"'); - expect(huskyPreCommitFile).toBe('#!/bin/sh\necho "Running lint-staged..."\npnpm exec lint-staged\n'); - const nextStepsMessage = consoleTranscript.info.at(-1) ?? ""; - expect(nextStepsMessage).not.toContain("Have your agent run this in the new project"); - expect(nextStepsMessage).not.toContain("complete the interactive prompt"); - expect(nextStepsMessage).not.toContain("pnpm proofkit"); - expect(nextStepsMessage).not.toContain("More ProofKit commands"); - expect(nextStepsMessage).toContain("\u001B["); - }); - - it("creates filemaker env and typegen config when explicit hosted inputs are provided", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-filemaker-")); - const layer = makeTestLayer({ - cwd, - packageManager: "pnpm", - }); - - const plan = planInit( - makeInitRequest({ - projectName: "filemaker-app", - scopedAppName: "filemaker-app", - appDir: "filemaker-app", - appType: "browser", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "hosted-otto", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - server: "https://example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await Effect.runPromise(layer(executeInitPlan(plan))); - - const projectDir = path.join(cwd, "filemaker-app"); - const { packageJson, proofkitJson, envFile, typegenConfig } = await readScaffoldArtifacts(projectDir); - - expect(packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - expect(proofkitJson.dataSources).toEqual([ - { - type: "fm", - name: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - }, - ]); - expect(envFile).toContain("FM_DATABASE=Contacts.fmp12"); - expect(envFile).toContain("FM_SERVER=https://example.com"); - expect(envFile).toContain("OTTO_API_KEY=dk_123"); - expect(typegenConfig).toContain("API_Contacts"); - expect(typegenConfig).toContain("Contacts"); - }); -}); diff --git a/packages/cli/tests/legacy-project-name-utils.test.ts b/packages/cli/tests/legacy-project-name-utils.test.ts deleted file mode 100644 index 07ebbca8..00000000 --- a/packages/cli/tests/legacy-project-name-utils.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { parseNameAndPath } from "~/utils/parseNameAndPath.js"; -import { validateAppName } from "~/utils/validateAppName.js"; - -describe("legacy project name utils", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("normalizes the current directory name when parsing '.'", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/My App"); - expect(parseNameAndPath(".")).toEqual(["my-app", "."]); - }); - - it("validates the normalized current directory name for '.'", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/My App"); - expect(validateAppName(".")).toBeUndefined(); - }); -}); diff --git a/packages/cli/tests/live-git-init.test.ts b/packages/cli/tests/live-git-init.test.ts deleted file mode 100644 index 5efa6dc2..00000000 --- a/packages/cli/tests/live-git-init.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { executeInitPlan } from "~/core/executeInitPlan.js"; -import { planInit } from "~/core/planInit.js"; -import { makeLiveLayer } from "~/services/live.js"; -import { getSharedTemplateDir, makeInitRequest } from "./init-fixtures.js"; - -const { execaMock, warnMock, successMock } = vi.hoisted(() => ({ - execaMock: vi.fn(), - warnMock: vi.fn(), - successMock: vi.fn(), -})); - -vi.mock("execa", () => ({ - execa: execaMock, -})); - -vi.mock("~/utils/prompts.js", () => ({ - confirmPrompt: vi.fn(), - spinner: vi.fn(), - isCancel: vi.fn(() => false), - log: { - error: vi.fn(), - info: vi.fn(), - success: successMock, - warn: warnMock, - }, - multiSearchSelectPrompt: vi.fn(), - note: vi.fn(), - passwordPrompt: vi.fn(), - searchSelectPrompt: vi.fn(), - selectPrompt: vi.fn(), - textPrompt: vi.fn(), -})); - -describe("live git init", () => { - beforeEach(() => { - vi.clearAllMocks(); - execaMock.mockImplementation((command: string, args: string[]) => { - if (command === "pnpm" && args[0] === "-v") { - return Promise.resolve({ stdout: "11.1.0" }); - } - - if (command === "git" && args[0] === "commit") { - throw new Error("Author identity unknown"); - } - - return Promise.resolve({ stdout: "", stderr: "" }); - }); - }); - - it("warns and continues when initial git commit fails", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-live-git-")); - const plan = planInit( - makeInitRequest({ - projectName: "git-warn-app", - scopedAppName: "git-warn-app", - appDir: "git-warn-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: false, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await expect( - Effect.runPromise(executeInitPlan(plan).pipe(makeLiveLayer({ cwd, debug: false, nonInteractive: true }))), - ).resolves.toBeDefined(); - - expect(execaMock).toHaveBeenCalledWith("git", ["init"], expect.objectContaining({ cwd: plan.targetDir })); - expect(execaMock).toHaveBeenCalledWith("git", ["add", "."], expect.objectContaining({ cwd: plan.targetDir })); - expect(execaMock).toHaveBeenCalledWith( - "git", - ["commit", "-m", "Initial commit"], - expect.objectContaining({ cwd: plan.targetDir }), - ); - expect(warnMock).toHaveBeenCalledWith("Git initial commit failed; continuing without commit."); - expect(successMock).toHaveBeenCalledWith(expect.stringContaining("Created git-warn-app")); - }); -}); diff --git a/packages/cli/tests/non-interactive.test.ts b/packages/cli/tests/non-interactive.test.ts deleted file mode 100644 index ddeafdd9..00000000 --- a/packages/cli/tests/non-interactive.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { detectNonInteractiveTerminal, resolveNonInteractiveMode } from "~/utils/nonInteractive.js"; - -describe("non-interactive detection", () => { - it("treats piped terminals as non-interactive", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: false, - stdoutIsTTY: true, - env: {}, - }), - ).toBe(true); - }); - - it("treats TERM=dumb as non-interactive", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: true, - stdoutIsTTY: true, - env: { TERM: "dumb" }, - }), - ).toBe(true); - }); - - it("treats coding-agent env vars as non-interactive even with a tty", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: true, - stdoutIsTTY: true, - env: { CODEX: "1" }, - }), - ).toBe(true); - }); - - it("treats OPENAI_CODEX as non-interactive even with a tty", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: true, - stdoutIsTTY: true, - env: { OPENAI_CODEX: "1" }, - }), - ).toBe(true); - }); - - it("keeps real terminals interactive when no signals are present", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: true, - stdoutIsTTY: true, - env: {}, - }), - ).toBe(false); - }); - - it("lets explicit flags force non-interactive mode", () => { - expect( - resolveNonInteractiveMode({ - nonInteractive: true, - stdinIsTTY: true, - stdoutIsTTY: true, - env: {}, - }), - ).toBe(true); - expect( - resolveNonInteractiveMode({ - CI: true, - stdinIsTTY: true, - stdoutIsTTY: true, - env: {}, - }), - ).toBe(true); - }); -}); diff --git a/packages/cli/tests/ottofms.test.ts b/packages/cli/tests/ottofms.test.ts deleted file mode 100644 index ab60999a..00000000 --- a/packages/cli/tests/ottofms.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import axios from "axios"; -import open from "open"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { getOttoFMSToken } from "~/cli/ottofms.js"; - -vi.mock("axios", () => ({ - default: { - get: vi.fn(), - delete: vi.fn(), - }, - AxiosError: class AxiosError extends Error {}, -})); - -vi.mock("open", () => ({ - default: vi.fn(), -})); - -vi.mock("~/cli/prompts.js", () => ({ - log: { - info: vi.fn(), - }, - spinner: () => ({ - start: vi.fn(), - stop: vi.fn(), - }), -})); - -describe("OttoFMS browser login", () => { - beforeEach(() => { - vi.useFakeTimers(); - vi.mocked(open).mockResolvedValue({} as Awaited>); - vi.mocked(axios.get).mockRejectedValue(new Error("pending")); - vi.mocked(axios.delete).mockResolvedValue({ data: {} }); - }); - - afterEach(() => { - vi.useRealTimers(); - vi.clearAllMocks(); - }); - - it("rejects when login polling times out", async () => { - const tokenPromise = getOttoFMSToken({ - url: new URL("https://example.com"), - }).catch((error: unknown) => error); - - await vi.advanceTimersByTimeAsync(180_000); - - await expect(tokenPromise).resolves.toMatchObject({ - message: "Login timed out", - }); - expect(vi.getTimerCount()).toBe(0); - }); -}); diff --git a/packages/cli/tests/planner.test.ts b/packages/cli/tests/planner.test.ts deleted file mode 100644 index 9b83918b..00000000 --- a/packages/cli/tests/planner.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { NODE_RUNTIME_VERSION } from "~/consts.js"; -import { planInit } from "~/core/planInit.js"; -import { - getFmdapiVersion, - getProofkitDependencyVersion, - getProofkitWebviewerVersion, - getTypegenVersion, - getVersion, -} from "~/utils/getProofKitVersion.js"; -import { makeInitRequest } from "./init-fixtures.js"; - -const proofkitFmdapiVersion = getProofkitDependencyVersion(getFmdapiVersion()); -const proofkitTypegenVersion = getProofkitDependencyVersion(getTypegenVersion()); -const proofkitWebviewerVersion = getProofkitDependencyVersion(getProofkitWebviewerVersion()); -const pnpm11WarningPattern = /pnpm.*11/i; - -describe("planInit", () => { - it("plans a browser scaffold", () => { - const plan = planInit(makeInitRequest(), { - templateDir: "/templates/browser", - packageManagerVersion: "11.0.0", - }); - - expect(plan.targetDir).toBe(path.resolve("/tmp/workspace", "demo-app")); - expect(plan.templateDir).toBe("/templates/browser"); - expect(plan.packageJson.name).toBe("demo-app"); - expect(plan.packageJson.proofkitMetadata).toEqual({ - initVersion: getVersion(), - scaffoldPackage: "@proofkit/cli", - }); - expect(plan.packageJson.devEngines?.packageManager).toEqual({ - name: "pnpm", - version: "^11.0.0", - onFail: "download", - }); - expect(plan.packageJson.devEngines?.runtime).toEqual({ - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }); - expect(plan.packageJson.engines).toEqual({ - node: NODE_RUNTIME_VERSION, - }); - expect(plan.settings.appType).toBe("browser"); - expect(plan.packageJson.devDependencies["@proofkit/cli"]).toBeUndefined(); - expect(plan.packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - expect(plan.tasks.runInstall).toBe(true); - expect(plan.tasks.runUltraciteInit).toBe(true); - expect(plan.tasks.runIntentInstall).toBe(true); - expect(plan.tasks.runFix).toBe(true); - expect(plan.tasks.runLint).toBe(true); - expect(plan.tasks.initializeGit).toBe(true); - expect(plan.tasks.bootstrapFileMaker).toBe(false); - expect(plan.tasks.checkWebViewerAddon).toBe(false); - expect(plan.writes).toContainEqual({ - path: path.resolve("/tmp/workspace", "demo-app", "pnpm-workspace.yaml"), - content: [ - "# This setting defines where in the repo your apps/packages that need installed dependancies exist. This of this as a list of paths to your package.json files. ", - "packages:", - ' - "."', - "", - "allowBuilds:", - ' "@parcel/watcher": true', - ' "esbuild": true', - ' "msgpackr-extract": true', - ' "msw": true', - ' "node": true', - ' "sharp": true', - "", - "trustPolicy: no-downgrade", - "", - "trustPolicyIgnoreAfter: 43200", - "", - "blockExoticSubdeps: true", - "", - ].join("\n"), - }); - }); - - it("plans a webviewer scaffold with no install and no git", () => { - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "none", - noInstall: true, - noGit: true, - }), - { - templateDir: "/templates/webviewer", - packageManagerVersion: "11.0.0", - }, - ); - - expect(plan.packageJson.dependencies["@proofkit/webviewer"]).toBe(proofkitWebviewerVersion); - expect(plan.packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - expect(plan.tasks.runInstall).toBe(false); - expect(plan.tasks.runUltraciteInit).toBe(true); - expect(plan.tasks.runIntentInstall).toBe(false); - expect(plan.tasks.runFix).toBe(false); - expect(plan.tasks.runLint).toBe(false); - expect(plan.tasks.initializeGit).toBe(false); - expect(plan.tasks.checkWebViewerAddon).toBe(true); - expect(plan.writes).toContainEqual({ - path: path.resolve("/tmp/workspace", "demo-app", "pnpm-workspace.yaml"), - content: [ - "# This setting defines where in the repo your apps/packages that need installed dependancies exist. This of this as a list of paths to your package.json files. ", - "packages:", - ' - "."', - "", - "allowBuilds:", - ' "@parcel/watcher": true', - ' "esbuild": true', - ' "msgpackr-extract": true', - ' "msw": true', - ' "node": true', - ' "sharp": false', - "", - "trustPolicy: no-downgrade", - "", - "trustPolicyIgnoreAfter: 43200", - "", - "blockExoticSubdeps: true", - "", - ].join("\n"), - }); - }); - - it("adds pnpm build approvals for pnpm 10", () => { - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "none", - }), - { - templateDir: "/templates/webviewer", - packageManagerVersion: "10.27.0", - }, - ); - - expect(plan.writes).toContainEqual( - expect.objectContaining({ - path: path.resolve("/tmp/workspace", "demo-app", "pnpm-workspace.yaml"), - content: expect.stringContaining(' "sharp": false'), - }), - ); - }); - - it("warns npm users to use pnpm 11 or greater", () => { - const plan = planInit( - makeInitRequest({ - packageManager: "npm", - }), - { - templateDir: "/templates/browser", - packageManagerVersion: "10.0.0", - }, - ); - - expect(plan.nextSteps).toEqual(expect.arrayContaining([expect.stringMatching(pnpm11WarningPattern)])); - }); - - it("writes npm minimum release age config for npm scaffolds", () => { - const plan = planInit( - makeInitRequest({ - packageManager: "npm", - }), - { - templateDir: "/templates/browser", - packageManagerVersion: "11.10.0", - }, - ); - - expect(plan.writes).toContainEqual({ - path: path.resolve("/tmp/workspace", "demo-app", ".npmrc"), - content: [ - "# Require npm package releases to be at least 24 hours old before install.", - "min-release-age=1", - "", - ].join("\n"), - }); - }); - - it("omits intent install from user-facing next steps", () => { - const cases = [ - ["npm", "npx @tanstack/intent@latest install"], - ["pnpm", "pnpx @tanstack/intent@latest install"], - ["yarn", "yarn dlx @tanstack/intent@latest install"], - ["bun", "bunx @tanstack/intent@latest install"], - ] as const; - - for (const [packageManager, nextStep] of cases) { - const plan = planInit(makeInitRequest({ packageManager }), { - templateDir: "/templates/browser", - }); - - expect(plan.nextSteps).not.toContain(nextStep); - } - }); - - it("adds fmdapi for browser filemaker scaffolds", () => { - const plan = planInit( - makeInitRequest({ - appType: "browser", - dataSource: "filemaker", - }), - { - templateDir: "/templates/browser", - }, - ); - - expect(plan.packageJson.dependencies["@proofkit/fmdapi"]).toBe(proofkitFmdapiVersion); - expect(plan.packageJson.dependencies.zod).toBe("^4"); - expect(plan.packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - }); - - it("plans filemaker bootstrap and initial codegen when inputs are explicit", () => { - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "filemaker", - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "hosted-otto", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - server: "https://example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }, - }), - { - templateDir: "/templates/webviewer", - }, - ); - - expect(plan.tasks.bootstrapFileMaker).toBe(true); - expect(plan.tasks.runInitialCodegen).toBe(true); - }); - - it("skips initial codegen for non-interactive webviewer runs without explicit inputs", () => { - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "filemaker", - }), - { - templateDir: "/templates/webviewer", - }, - ); - - expect(plan.tasks.bootstrapFileMaker).toBe(true); - expect(plan.tasks.runInitialCodegen).toBe(false); - }); - - it("skips initial codegen when install is disabled", () => { - const plan = planInit( - makeInitRequest({ - appType: "browser", - dataSource: "filemaker", - noInstall: true, - hasExplicitFileMakerInputs: true, - }), - { - templateDir: "/templates/browser", - }, - ); - - expect(plan.tasks.bootstrapFileMaker).toBe(true); - expect(plan.tasks.runInstall).toBe(false); - expect(plan.tasks.runInitialCodegen).toBe(false); - expect(plan.commands.some((command) => command.type === "codegen")).toBe(false); - }); -}); diff --git a/packages/cli/tests/project-name.test.ts b/packages/cli/tests/project-name.test.ts deleted file mode 100644 index ed10cc3b..00000000 --- a/packages/cli/tests/project-name.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { parseNameAndPath, validateAppName } from "~/utils/projectName.js"; - -describe("projectName utils", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("normalizes Windows-style separators when parsing the app name and directory", () => { - expect(parseNameAndPath("apps\\my-app")).toEqual(["my-app", "apps/my-app"]); - expect(parseNameAndPath(".\\my-app\\")).toEqual(["my-app", "./my-app"]); - }); - - it("converts spaces to dashes when parsing the app name and directory", () => { - expect(parseNameAndPath("my app")).toEqual(["my-app", "my-app"]); - expect(validateAppName("my app")).toBeUndefined(); - }); - - it("preserves leading directory casing while normalizing only the package segment", () => { - expect(parseNameAndPath("Apps Folder/My App")).toEqual(["my-app", "Apps Folder/my-app"]); - }); - - it("normalizes scoped package segments without lowercasing leading directories", () => { - expect(parseNameAndPath("Apps Folder/@My Scope/My App")).toEqual([ - "@my-scope/my-app", - "Apps Folder/@my-scope/my-app", - ]); - }); - - it("validates the actual current directory name when projectName is '.'", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/My App"); - expect(validateAppName(".")).toBeUndefined(); - }); - - it("accepts '.' when the current directory name is valid", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/my-app"); - expect(validateAppName(".")).toBeUndefined(); - }); - - it("normalizes the current directory name when parsing '.'", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/My App"); - expect(parseNameAndPath(".")).toEqual(["my-app", "."]); - }); -}); diff --git a/packages/cli/tests/prompts.test.ts b/packages/cli/tests/prompts.test.ts deleted file mode 100644 index fd7106cc..00000000 --- a/packages/cli/tests/prompts.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { filterSearchOptions } from "~/utils/prompts.js"; - -describe("filterSearchOptions", () => { - const options = [ - { - value: "Contacts.fmp12", - label: "Contacts.fmp12", - hint: "open", - keywords: ["contacts", "reporting"], - }, - { - value: "Invoices.fmp12", - label: "Invoices.fmp12", - hint: "closed", - keywords: ["billing"], - disabled: "Already connected", - }, - ] as const; - - it("matches on labels, hints, and keywords", () => { - expect(filterSearchOptions(options, "reporting").map((option) => option.value)).toEqual(["Contacts.fmp12"]); - expect(filterSearchOptions(options, "closed").map((option) => option.value)).toEqual(["Invoices.fmp12"]); - }); - - it("returns all options when the search term is empty", () => { - expect(filterSearchOptions(options, "")).toEqual(options); - expect(filterSearchOptions(options, " ")).toEqual(options); - expect(filterSearchOptions(options, undefined)).toEqual(options); - }); - - it("returns an empty list when nothing matches", () => { - expect(filterSearchOptions(options, "missing")).toEqual([]); - }); -}); diff --git a/packages/cli/tests/render-failure.test.ts b/packages/cli/tests/render-failure.test.ts deleted file mode 100644 index 34013d5d..00000000 --- a/packages/cli/tests/render-failure.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Cause } from "effect"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { NonInteractiveInputError } from "~/core/errors.js"; -import { renderFailure } from "~/index.js"; - -describe("renderFailure", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("renders tagged cli errors without squashing", () => { - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - - renderFailure( - Cause.fail( - new NonInteractiveInputError({ - message: "typed failure", - }), - ), - false, - ); - - expect(errorSpy).toHaveBeenCalledWith("typed failure"); - }); - - it("renders unknown defects via squash", () => { - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - - renderFailure(Cause.die(new Error("boom")), false); - - expect(errorSpy).toHaveBeenCalledWith("boom"); - }); -}); diff --git a/packages/cli/tests/resolve-init.test.ts b/packages/cli/tests/resolve-init.test.ts deleted file mode 100644 index 84622dc9..00000000 --- a/packages/cli/tests/resolve-init.test.ts +++ /dev/null @@ -1,859 +0,0 @@ -import { Effect } from "effect"; -import { describe, expect, it } from "vitest"; -import { - CliValidationError, - ExternalCommandError, - FileMakerSetupError, - NonInteractiveInputError, - UserCancelledError, -} from "~/core/errors.js"; -import { resolveInitRequest } from "~/core/resolveInitRequest.js"; -import { getFailure } from "./effect-test-utils.js"; -import { type ConsoleTranscript, makeTestLayer, type PromptTranscript } from "./test-layer.js"; - -describe("resolveInitRequest", () => { - it("uses pnpm when npm invoked and pnpm is installed", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: true, - importAlias: "~/", - CI: true, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "npm", - }), - ), - ); - - expect(request.packageManager).toBe("pnpm"); - }); - - it("aborts interactively when npm invoked and pnpm is missing", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: true, - importAlias: "~/", - CI: false, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "npm", - nonInteractive: false, - failures: { - packageManagerGetVersion: { - pnpm: new ExternalCommandError({ - message: "pnpm not found", - command: "pnpm", - args: ["-v"], - cwd: "/tmp", - }), - }, - }, - }), - ), - ), - ).toMatchObject( - new UserCancelledError({ - message: "User aborted to install pnpm first.", - }), - ); - }); - - it("continues with npm when warning is ignored", async () => { - const promptTranscript: PromptTranscript = { - text: [], - password: [], - select: [], - searchSelect: [], - multiSearchSelect: [], - confirm: [], - }; - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: true, - importAlias: "~/", - CI: false, - appType: "browser", - dataSource: "none", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "npm", - nonInteractive: false, - prompts: { - select: ["continue"], - }, - promptTranscript, - failures: { - packageManagerGetVersion: { - pnpm: new ExternalCommandError({ - message: "pnpm not found", - command: "pnpm", - args: ["-v"], - cwd: "/tmp", - }), - }, - }, - }), - ), - ); - - expect(request.packageManager).toBe("npm"); - expect(promptTranscript.select[0]?.message).toContain("https://pnpm.io/installation"); - }); - - it("continues with npm non-interactively when pnpm is missing", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: true, - importAlias: "~/", - CI: true, - appType: "browser", - dataSource: "none", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "npm", - failures: { - packageManagerGetVersion: { - pnpm: new ExternalCommandError({ - message: "pnpm not found", - command: "pnpm", - args: ["-v"], - cwd: "/tmp", - }), - }, - }, - }), - ), - ); - - expect(request.packageManager).toBe("npm"); - }); - - it("fails for missing project name in non-interactive mode", async () => { - expect( - await getFailure( - resolveInitRequest(undefined, { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: "Project name is required in non-interactive mode.", - }), - ); - }); - - it("fails for incomplete non-interactive filemaker inputs", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "browser", - dataSource: "filemaker", - server: "https://example.com", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: "Missing required FileMaker inputs in non-interactive mode: --file-name, --data-api-key.", - }), - ); - }); - - it("fails when only one of layout-name and schema-name is provided", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "browser", - dataSource: "filemaker", - layoutName: "API_Contacts", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ), - ).toMatchObject( - new CliValidationError({ - message: "Both --layout-name and --schema-name must be provided together.", - }), - ); - }); - - it("resolves an interactive filemaker request from prompt responses", async () => { - const request = await Effect.runPromise( - resolveInitRequest(undefined, { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - prompts: { - text: ["interactive-app", "https://fm.example.com", "reportingContacts"], - select: ["webviewer", "hosted"], - searchSelect: ["Contacts.fmp12", "dk_existing", "API_Contacts"], - confirm: [true], - }, - }), - ), - ); - - expect(request.projectName).toBe("interactive-app"); - expect(request.appType).toBe("webviewer"); - expect(request.dataSource).toBe("filemaker"); - expect(request.fileMaker).toMatchObject({ - mode: "hosted-otto", - server: "https://fm.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_existing", - schemaName: "reportingContacts", - }); - }); - - it("marks explicit filemaker inputs in non-interactive mode", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - server: "https://fm.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ); - - expect(request.hasExplicitFileMakerInputs).toBe(true); - expect(request.fileMaker).toMatchObject({ - mode: "hosted-otto", - server: "https://fm.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }); - }); - - it("normalizes a non-interactive layout name to the live FileMaker casing", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - server: "https://fm.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "contacts", - schemaName: "Contacts", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - layoutName: "Contacts", - schemaName: "Contacts", - }); - }); - - it("uses local fm http for webviewer setup when available", async () => { - const consoleTranscript: ConsoleTranscript = { - info: [], - warn: [], - error: [], - success: [], - note: [], - }; - const tracker = { - commands: [], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - localFmMcpAuthorizations: [], - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - console: consoleTranscript, - tracker, - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["LocalFile.fmp12"], - }, - }, - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - fileName: "LocalFile.fmp12", - }); - expect(tracker.localFmMcpAuthorizations).toEqual([ - { - clientName: "ProofKit CLI (demo)", - clientDescription: "ProofKit CLI wants to read layouts from your FileMaker file to help set up your project.", - }, - ]); - expect(consoleTranscript.info).toContain("Using ProofKit plugin file: LocalFile.fmp12"); - }); - - it("skips local fm authorization when proofkit token is provided", async () => { - const tracker = { - commands: [], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - localFmMcpAuthorizations: [], - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - proofkitToken: "provided-token", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - tracker, - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["LocalFile.fmp12"], - }, - }, - }), - ), - ); - - expect(request.proofkitToken).toBe("provided-token"); - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - proofkitToken: "provided-token", - }); - expect(tracker.localFmMcpAuthorizations).toEqual([]); - }); - - it("uses FM_MCP_SESSION_ID as proofkit token fallback", async () => { - const original = process.env.FM_MCP_SESSION_ID; - process.env.FM_MCP_SESSION_ID = "env-token"; - try { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["LocalFile.fmp12"], - }, - }, - }), - ), - ); - - expect(request.proofkitToken).toBe("env-token"); - } finally { - if (original === undefined) { - delete process.env.FM_MCP_SESSION_ID; - } else { - process.env.FM_MCP_SESSION_ID = original; - } - } - }); - - it("asks which local FileMaker file to use when multiple are open", async () => { - const promptTranscript: PromptTranscript = { - text: [], - password: [], - select: [], - searchSelect: [], - multiSearchSelect: [], - confirm: [], - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - prompts: { - searchSelect: ["B.fmp12"], - }, - promptTranscript, - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["A.fmp12", "B.fmp12"], - }, - }, - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - fileName: "B.fmp12", - }); - expect(promptTranscript.searchSelect).toContain( - "Multiple FileMaker files are open. Which file should ProofKit use?", - ); - }); - - it("fails in non-interactive mode when multiple local FileMaker files are open without --file-name", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["A.fmp12", "B.fmp12"], - }, - }, - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: - "Multiple FileMaker files are connected to the ProofKit plugin. Pass --file-name with one of: A.fmp12, B.fmp12.", - }), - ); - }); - - it("uses --file-name for non-interactive local MCP selection when multiple files are open", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - fileName: "B.fmp12", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["A.fmp12", "B.fmp12"], - }, - }, - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - fileName: "B.fmp12", - }); - }); - - it("fails when --file-name does not match a connected local FileMaker file", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - fileName: "Missing.fmp12", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["A.fmp12", "B.fmp12"], - }, - }, - }), - ), - ), - ).toMatchObject( - new FileMakerSetupError({ - message: - 'FileMaker file "Missing.fmp12" is not currently connected to the ProofKit plugin. Connected files: A.fmp12, B.fmp12.', - }), - ); - }); - - it("propagates a typed hosted FileMaker validation error", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "browser", - dataSource: "filemaker", - server: "https://bad.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - failures: { - validateHostedServerUrl: new FileMakerSetupError({ - message: "Invalid FileMaker Server URL: https://bad.example.com", - }), - }, - }), - ), - ), - ).toMatchObject( - new FileMakerSetupError({ - message: "Invalid FileMaker Server URL: https://bad.example.com", - }), - ); - }); - - it("prompts to retry when ProofKit plugin is installed but no FileMaker file is connected", async () => { - const promptTranscript: PromptTranscript = { - text: [], - password: [], - select: [], - searchSelect: [], - multiSearchSelect: [], - confirm: [], - }; - const tracker = { - commands: [], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - addonInstalls: 0, - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - tracker, - prompts: { - select: ["skip"], - }, - promptTranscript, - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: [], - }, - }, - }), - ), - ); - - expect(request.fileMaker).toBeUndefined(); - expect(request.skipFileMakerSetup).toBe(true); - expect(tracker.addonInstalls).toBe(1); - expect(promptTranscript.select).toContainEqual({ - message: - "ProofKit plugin is installed, but no FileMaker file is connected yet. Install the ProofKit Web Viewer add-on in your FileMaker file, run the add-on connection script, then choose how to continue.", - options: ["retry", "hosted", "skip"], - }); - }); - - it("retries local MCP detection, then reports the connected file", async () => { - const consoleTranscript: ConsoleTranscript = { - info: [], - warn: [], - error: [], - success: [], - note: [], - }; - const tracker = { - commands: [], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - addonInstalls: 0, - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - tracker, - prompts: { - select: ["retry"], - }, - console: consoleTranscript, - fileMaker: { - localFmMcp: [ - { - healthy: true, - connectedFiles: [], - }, - { - healthy: true, - connectedFiles: ["RetryConnected.fmp12"], - }, - ], - }, - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - fileName: "RetryConnected.fmp12", - }); - expect(tracker.addonInstalls).toBe(2); - expect(consoleTranscript.info).toContain("Using ProofKit plugin file: RetryConnected.fmp12"); - }); - - it("fails with a specific non-interactive error when ProofKit plugin is installed but no FileMaker file is connected", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: [], - }, - }, - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: - "ProofKit plugin was detected, but no FileMaker file is connected. Install the ProofKit plugin, install the ProofKit Web Viewer add-on in your FileMaker file, then run the add-on connection script and rerun. Or pass --server.", - }), - ); - }); - - it("propagates a typed demo deployment error", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "browser", - dataSource: "filemaker", - server: "https://fm.example.com", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - prompts: { - searchSelect: ["$deploy-demo"], - }, - failures: { - deployDemoFile: new FileMakerSetupError({ - message: "ProofKit Demo deployment timed out after 5 minutes.", - }), - }, - }), - ), - ), - ).toMatchObject( - new FileMakerSetupError({ - message: "ProofKit Demo deployment timed out after 5 minutes.", - }), - ); - }); - - it("fails with a typed cancelation error when a prompt is cancelled", async () => { - expect( - await getFailure( - resolveInitRequest(undefined, { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - prompts: { - text: ["__cancel__"], - }, - }), - ), - ), - ).toMatchObject( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - }); -}); diff --git a/packages/cli/tests/setup.ts b/packages/cli/tests/setup.ts deleted file mode 100644 index bff6180a..00000000 --- a/packages/cli/tests/setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { execSync } from "node:child_process"; -import path, { join } from "node:path"; -import dotenv from "dotenv"; -import { beforeAll } from "vitest"; - -beforeAll(() => { - // Ensure test environment variables are loaded - dotenv.config({ path: path.resolve(import.meta.dirname, "../.env.test") }); - process.env.PROOFKIT_SKIP_VERSION_CHECK = "1"; -}); - -// Build the CLI before running any tests -execSync("pnpm build", { cwd: join(import.meta.dirname, "..") }); diff --git a/packages/cli/tests/test-layer.ts b/packages/cli/tests/test-layer.ts deleted file mode 100644 index e0c7688a..00000000 --- a/packages/cli/tests/test-layer.ts +++ /dev/null @@ -1,618 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import type { Effect as Fx } from "effect"; -import { Effect, Layer } from "effect"; -import fs from "fs-extra"; -import { - CliContext, - CodegenService, - ConsoleService, - FileMakerService, - FileSystemService, - GitService, - PackageManagerService, - ProcessService, - PromptService, - SettingsService, - TemplateService, -} from "~/core/context.js"; -import { type ExternalCommandError, FileMakerSetupError, FileSystemError, UserCancelledError } from "~/core/errors.js"; -import type { AppType, FileMakerInputs, ProofKitSettings, UIType } from "~/core/types.js"; -import type { PackageManager } from "~/utils/packageManager.js"; -import { createDataSourceEnvNames, updateTypegenConfig } from "~/utils/projectFiles.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export interface PromptScript { - text?: string[]; - select?: Array; - confirm?: Array; - password?: string[]; - searchSelect?: string[]; - multiSearchSelect?: string[][]; -} - -export interface ConsoleTranscript { - info: string[]; - warn: string[]; - error: string[]; - success: string[]; - note: Array<{ message: string; title?: string }>; -} - -export interface PromptTranscript { - text: string[]; - password: string[]; - select: Array<{ - message: string; - options: string[]; - }>; - searchSelect: string[]; - multiSearchSelect: string[]; - confirm: string[]; -} - -export function makeTestLayer(options: { - cwd: string; - packageManager: PackageManager; - nonInteractive?: boolean; - prompts?: PromptScript; - console?: ConsoleTranscript; - failProcessCommand?: string; - promptTranscript?: PromptTranscript; - tracker?: { - commands: string[]; - gitInits: number; - codegens: number; - codegenTokens?: Array; - filemakerBootstraps: number; - addonInstalls?: number; - localFmMcpAuthorizations?: { - clientName: string; - clientDescription: string; - }[]; - }; - fileMaker?: { - localFmMcp?: - | { - healthy: boolean; - baseUrl?: string; - connectedFiles?: string[]; - } - | Array<{ - healthy: boolean; - baseUrl?: string; - connectedFiles?: string[]; - }>; - }; - failures?: { - processRun?: unknown; - gitInitialize?: unknown; - codegenRun?: unknown; - validateHostedServerUrl?: unknown; - deployDemoFile?: unknown; - packageManagerGetVersion?: Partial>; - }; -}) { - const tracker = options.tracker; - const promptScript = { - text: [...(options.prompts?.text ?? [])], - select: [...(options.prompts?.select ?? [])], - confirm: [...(options.prompts?.confirm ?? [])], - password: [...(options.prompts?.password ?? [])], - searchSelect: [...(options.prompts?.searchSelect ?? [])], - multiSearchSelect: [...(options.prompts?.multiSearchSelect ?? [])], - }; - const consoleTranscript = options.console; - let localFmMcpScript: - | Array<{ - healthy: boolean; - baseUrl?: string; - connectedFiles?: string[]; - }> - | undefined; - if (Array.isArray(options.fileMaker?.localFmMcp)) { - localFmMcpScript = [...options.fileMaker.localFmMcp]; - } else if (options.fileMaker?.localFmMcp) { - localFmMcpScript = [options.fileMaker.localFmMcp]; - } else { - localFmMcpScript = []; - } - let lastLocalFmMcp = localFmMcpScript[0]; - - const layer = Layer.mergeAll( - Layer.succeed(CliContext, { - cwd: options.cwd, - debug: false, - nonInteractive: options.nonInteractive ?? true, - packageManager: options.packageManager, - }), - Layer.succeed(PromptService, { - text: ({ message, defaultValue }: { message: string; defaultValue?: string }) => { - options.promptTranscript?.text.push(message); - const next = promptScript.text.shift(); - if (next === "__cancel__") { - return Promise.reject(new UserCancelledError({ message: "User aborted the operation" })); - } - return Promise.resolve(next ?? defaultValue ?? "value"); - }, - password: ({ message }: { message: string }) => { - options.promptTranscript?.password.push(message); - const next = promptScript.password.shift(); - if (next === "__cancel__") { - return Promise.reject(new UserCancelledError({ message: "User aborted the operation" })); - } - return Promise.resolve(next ?? "password"); - }, - select: ({ message, options: selectOptions }: { message: string; options: { value: T }[] }) => { - options.promptTranscript?.select.push({ - message, - options: selectOptions.map((option) => option.value), - }); - const next = promptScript.select.shift(); - if (next === "__cancel__") { - return Promise.reject(new UserCancelledError({ message: "User aborted the operation" })); - } - if (next === "__cancel_value__") { - return Promise.resolve(Symbol.for("@proofkit/new/prompt-cancelled") as unknown as T); - } - if (next) { - const match = selectOptions.find((option) => option.value === next); - if (match) { - return Promise.resolve(match.value); - } - } - return Promise.resolve(selectOptions[0]?.value ?? ("" as T)); - }, - searchSelect: ({ - message, - options: searchOptions, - }: { - message: string; - options: { value: T }[]; - }) => { - options.promptTranscript?.searchSelect.push(message); - const next = promptScript.searchSelect.shift(); - if (next === "__cancel__") { - return Promise.reject(new UserCancelledError({ message: "User aborted the operation" })); - } - if (next) { - const match = searchOptions.find((option) => option.value === next); - if (match) { - return Promise.resolve(match.value); - } - } - return Promise.resolve(searchOptions[0]?.value ?? ("" as T)); - }, - multiSearchSelect: ({ - message, - options: searchOptions, - }: { - message: string; - options: { value: T }[]; - }) => { - options.promptTranscript?.multiSearchSelect.push(message); - const next = promptScript.multiSearchSelect.shift(); - if (next) { - return Promise.resolve( - next.filter((value): value is T => searchOptions.some((option) => option.value === value)), - ); - } - return Promise.resolve(searchOptions.slice(0, 1).map((option) => option.value)); - }, - confirm: ({ message, initialValue }: { message: string; initialValue?: boolean }) => { - options.promptTranscript?.confirm.push(message); - const next = promptScript.confirm.shift(); - if (next === "__cancel_value__") { - return Promise.resolve(Symbol.for("@proofkit/new/prompt-cancelled") as unknown as boolean); - } - return Promise.resolve(next ?? initialValue ?? false); - }, - }), - Layer.succeed(ConsoleService, { - info: (message: string) => { - consoleTranscript?.info.push(message); - }, - warn: (message: string) => { - consoleTranscript?.warn.push(message); - }, - error: (message: string) => { - consoleTranscript?.error.push(message); - }, - success: (message: string) => { - consoleTranscript?.success.push(message); - }, - note: (message: string, title?: string) => { - consoleTranscript?.note.push({ message, title }); - }, - }), - Layer.succeed(FileSystemService, { - exists: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.pathExists(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system exists failed for ${targetPath}.`, - operation: "exists", - path: targetPath, - cause, - }), - }), - readdir: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.readdir(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system readdir failed for ${targetPath}.`, - operation: "readdir", - path: targetPath, - cause, - }), - }), - ensureDir: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.ensureDir(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system ensureDir failed for ${targetPath}.`, - operation: "ensureDir", - path: targetPath, - cause, - }), - }), - emptyDir: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.emptyDir(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system emptyDir failed for ${targetPath}.`, - operation: "emptyDir", - path: targetPath, - cause, - }), - }), - copyDir: (from: string, to: string, opts?: { overwrite?: boolean }) => - Effect.tryPromise({ - try: () => fs.copy(from, to, { overwrite: opts?.overwrite ?? true }), - catch: (cause) => - new FileSystemError({ - message: `File system copyDir failed for ${from} -> ${to}.`, - operation: "copyDir", - path: `${from} -> ${to}`, - cause, - }), - }), - rename: (from: string, to: string) => - Effect.tryPromise({ - try: () => fs.rename(from, to), - catch: (cause) => - new FileSystemError({ - message: `File system rename failed for ${from} -> ${to}.`, - operation: "rename", - path: `${from} -> ${to}`, - cause, - }), - }), - remove: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.remove(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system remove failed for ${targetPath}.`, - operation: "remove", - path: targetPath, - cause, - }), - }), - readJson: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.readJson(targetPath) as Promise, - catch: (cause) => - new FileSystemError({ - message: `File system readJson failed for ${targetPath}.`, - operation: "readJson", - path: targetPath, - cause, - }), - }), - writeJson: (targetPath: string, value: unknown) => - Effect.tryPromise({ - try: () => fs.writeJson(targetPath, value, { spaces: 2 }), - catch: (cause) => - new FileSystemError({ - message: `File system writeJson failed for ${targetPath}.`, - operation: "writeJson", - path: targetPath, - cause, - }), - }), - writeFile: (targetPath: string, content: string) => - Effect.tryPromise({ - try: () => fs.writeFile(targetPath, content, "utf8"), - catch: (cause) => - new FileSystemError({ - message: `File system writeFile failed for ${targetPath}.`, - operation: "writeFile", - path: targetPath, - cause, - }), - }), - readFile: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.readFile(targetPath, "utf8"), - catch: (cause) => - new FileSystemError({ - message: `File system readFile failed for ${targetPath}.`, - operation: "readFile", - path: targetPath, - cause, - }), - }), - }), - Layer.succeed(TemplateService, { - getTemplateDir: (appType: AppType, _ui: UIType) => { - let templateName = "nextjs-shadcn"; - if (appType === "webviewer") { - templateName = "vite-wv"; - } - return path.resolve(__dirname, `../template/${templateName}`); - }, - }), - Layer.succeed(PackageManagerService, { - getVersion: (packageManager: PackageManager) => { - const failure = options.failures?.packageManagerGetVersion?.[packageManager]; - if (failure) { - return Effect.fail(failure as ExternalCommandError); - } - return Effect.succeed("11.1.0"); - }, - }), - Layer.succeed(ProcessService, { - run: (command: string, args: string[]) => { - const processCommand = [command, ...args].join(" "); - tracker?.commands.push(processCommand); - const processRunFailure = options.failures?.processRun; - if (options.failProcessCommand === processCommand) { - if (!processRunFailure) { - throw new Error("makeTestLayer requires failures.processRun when failProcessCommand is set."); - } - return Effect.fail(processRunFailure as ExternalCommandError); - } - if (!options.failProcessCommand && processRunFailure) { - return Effect.fail(processRunFailure as ExternalCommandError); - } - return Effect.succeed({ stdout: "", stderr: "" }); - }, - }), - Layer.succeed(GitService, { - initialize: () => { - if (tracker) { - tracker.gitInits += 1; - } - if (options.failures?.gitInitialize) { - return Effect.fail(options.failures.gitInitialize as ExternalCommandError); - } - return Effect.void; - }, - }), - Layer.succeed(SettingsService, { - writeSettings: (projectDir: string, settings: ProofKitSettings) => - Effect.tryPromise({ - try: () => - fs.writeJson(path.join(projectDir, "proofkit.json"), settings, { - spaces: 2, - }), - catch: (cause) => - new FileSystemError({ - message: "Unable to write ProofKit settings.", - operation: "writeSettings", - path: path.join(projectDir, "proofkit.json"), - cause, - }), - }), - appendEnvVars: (projectDir: string, vars: Record) => - Effect.tryPromise({ - try: async () => { - const envPath = path.join(projectDir, ".env"); - const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, "utf8") : ""; - const additions = Object.entries(vars) - .map(([name, value]) => `${name}=${value}`) - .join("\n"); - await fs.writeFile( - envPath, - [existing.trimEnd(), additions].filter(Boolean).join("\n").concat("\n"), - "utf8", - ); - }, - catch: (cause) => - new FileSystemError({ - message: "Unable to append env vars.", - operation: "appendEnvVars", - path: path.join(projectDir, ".env"), - cause, - }), - }), - }), - Layer.succeed(FileMakerService, { - detectLocalFmMcp: () => { - const next = localFmMcpScript.shift() ?? lastLocalFmMcp; - lastLocalFmMcp = next; - return Effect.succeed({ - baseUrl: next?.baseUrl ?? "http://127.0.0.1:1365", - healthy: next?.healthy ?? false, - connectedFiles: next?.connectedFiles ?? [], - }); - }, - installLocalWebViewerAddon: () => { - if (tracker) { - tracker.addonInstalls = (tracker.addonInstalls ?? 0) + 1; - } - return Effect.void; - }, - authorizeLocalFmMcp: (input) => { - tracker?.localFmMcpAuthorizations?.push({ - clientName: input.clientName, - clientDescription: input.clientDescription, - }); - return Effect.succeed({ - sessionToken: "test-session-token", - }); - }, - validateHostedServerUrl: (serverUrl: string) => { - if (options.failures?.validateHostedServerUrl) { - return Effect.fail(options.failures.validateHostedServerUrl as FileMakerSetupError); - } - return Effect.succeed({ - normalizedUrl: serverUrl, - versions: { - fmsVersion: "21.0.0", - ottoVersion: "4.8.0", - }, - }); - }, - getOttoFMSToken: () => Effect.succeed({ token: "admin_token" }), - listFiles: () => Effect.succeed([{ filename: "Contacts.fmp12", status: "open" }]), - listAPIKeys: () => - Effect.succeed([ - { - key: "dk_existing", - user: "Admin", - database: "Contacts.fmp12", - label: "Existing key", - }, - ]), - createDataAPIKeyWithCredentials: () => Effect.succeed({ apiKey: "dk_created" }), - deployDemoFile: () => { - if (options.failures?.deployDemoFile) { - return Effect.fail(options.failures.deployDemoFile as FileMakerSetupError); - } - return Effect.succeed({ - apiKey: "dk_demo", - filename: "ProofKitDemo.fmp12", - }); - }, - listLayouts: () => Effect.succeed(["API_Contacts", "Contacts"]), - createFileMakerBootstrapArtifacts: (settings: ProofKitSettings, inputs: FileMakerInputs, appType: AppType) => { - const envNames = createDataSourceEnvNames("filemaker"); - return Effect.succeed({ - settings: { - ...settings, - dataSources: [ - ...settings.dataSources, - { - type: "fm", - name: "filemaker", - envNames, - }, - ], - }, - envVars: - inputs.mode === "hosted-otto" - ? { - [envNames.database]: inputs.fileName, - [envNames.server]: inputs.server, - [envNames.apiKey]: inputs.dataApiKey, - } - : {}, - envSchemaEntries: - inputs.mode === "hosted-otto" - ? [ - { - name: envNames.database, - zodSchema: 'z.string().endsWith(".fmp12")', - defaultValue: inputs.fileName, - }, - { - name: envNames.server, - zodSchema: "z.string().url()", - defaultValue: inputs.server, - }, - { - name: envNames.apiKey, - zodSchema: 'z.string().startsWith("dk_")', - defaultValue: inputs.dataApiKey, - }, - ] - : [], - typegenConfig: { - mode: inputs.mode, - dataSourceName: "filemaker", - envNames: inputs.mode === "hosted-otto" ? envNames : undefined, - fmMcpBaseUrl: inputs.mode === "local-fm-mcp" ? inputs.fmMcpBaseUrl : undefined, - connectedFileName: inputs.mode === "local-fm-mcp" ? inputs.fileName : undefined, - layoutName: inputs.layoutName, - schemaName: inputs.schemaName, - appType, - }, - }); - }, - bootstrap: (projectDir: string, settings: ProofKitSettings, inputs: FileMakerInputs, appType: AppType) => - Effect.tryPromise({ - try: async () => { - if (tracker) { - tracker.filemakerBootstraps += 1; - } - const nextSettings: ProofKitSettings = { - ...settings, - dataSources: [ - ...settings.dataSources, - { - type: "fm", - name: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - }, - ], - }; - if (inputs.mode === "hosted-otto") { - const envPath = path.join(projectDir, ".env"); - const content = (await fs.readFile(envPath, "utf8")).concat( - `FM_DATABASE=${inputs.fileName}\nFM_SERVER=${inputs.server}\nOTTO_API_KEY=${inputs.dataApiKey}\n`, - ); - await fs.writeFile(envPath, content, "utf8"); - } - await updateTypegenConfig( - { - exists: async (targetPath: string) => fs.pathExists(targetPath), - readFile: async (targetPath: string) => fs.readFile(targetPath, "utf8"), - writeFile: async (targetPath: string, content: string) => fs.writeFile(targetPath, content, "utf8"), - }, - projectDir, - { - appType, - dataSourceName: "filemaker", - envNames: inputs.mode === "hosted-otto" ? createDataSourceEnvNames("filemaker") : undefined, - fmMcpBaseUrl: inputs.mode === "local-fm-mcp" ? inputs.fmMcpBaseUrl : undefined, - connectedFileName: inputs.mode === "local-fm-mcp" ? inputs.fileName : undefined, - layoutName: inputs.layoutName, - schemaName: inputs.schemaName, - }, - ); - return nextSettings; - }, - catch: (cause) => - new FileMakerSetupError({ - message: "Unable to bootstrap FileMaker in test layer.", - cause, - }), - }), - }), - Layer.succeed(CodegenService, { - runInitial: (_projectDir, _packageManager, proofkitToken) => { - if (tracker) { - tracker.codegens += 1; - tracker.codegenTokens?.push(proofkitToken); - } - if (options.failures?.codegenRun) { - return Effect.fail(options.failures.codegenRun as ExternalCommandError); - } - return Effect.void; - }, - }), - ); - - return (effect: Fx.Effect) => Effect.provide(effect, layer); -} diff --git a/packages/cli/tests/test-utils.ts b/packages/cli/tests/test-utils.ts deleted file mode 100644 index ddbd3d79..00000000 --- a/packages/cli/tests/test-utils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { execSync } from "node:child_process"; -import { readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -function execSmokeCommand(command: string, options: Parameters[1]) { - try { - return execSync(command, { - ...options, - stdio: "pipe", - encoding: "utf-8", - }); - } catch (error) { - if (error && typeof error === "object") { - const outputError = error as { stdout?: unknown; stderr?: unknown }; - if (typeof outputError.stdout === "string" && outputError.stdout.length > 0) { - console.error(outputError.stdout); - } - if (typeof outputError.stderr === "string" && outputError.stderr.length > 0) { - console.error(outputError.stderr); - } - } - throw error; - } -} - -/** - * Smoke-test helper only: swap workspace refs to published tags so install/build - * validates what end users can actually fetch from the registry. - */ -function applyPublishedProofkitVersionsForSmoke(projectDir: string): void { - const pkgPath = join(projectDir, "package.json"); - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - - const replaceProofkitVersions = (deps: Record | undefined) => { - if (!deps) { - return; - } - for (const name of Object.keys(deps)) { - if (name.startsWith("@proofkit/")) { - console.log(` Replacing ${name}@${deps[name]} with latest`); - deps[name] = "latest"; - } - } - }; - - console.log("Using latest published @proofkit/* versions..."); - replaceProofkitVersions(pkg.dependencies); - replaceProofkitVersions(pkg.devDependencies); - - writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); -} - -/** - * Verifies that a project at the given directory can be built without errors - * @param projectDir The directory containing the project to build - * @throws If the build fails - */ -export function verifySmokeProjectBuilds(projectDir: string): void { - console.log(`\nVerifying project build in ${projectDir}...`); - - try { - // Smoke tests intentionally validate published package installability. - applyPublishedProofkitVersionsForSmoke(projectDir); - - console.log("Installing dependencies..."); - execSmokeCommand("pnpm install --prefer-offline --no-frozen-lockfile", { - cwd: projectDir, - env: { - ...process.env, - PNPM_DEBUG: "1", // Enable debug logging - }, - }); - - console.log("Building project..."); - execSmokeCommand("pnpm build", { - cwd: projectDir, - env: { - ...process.env, - NEXT_TELEMETRY_DISABLED: "1", - }, - }); - } catch (error) { - console.error("Build process failed:", error); - throw error; - } -} diff --git a/packages/cli/tests/webviewer-apps.test.ts b/packages/cli/tests/webviewer-apps.test.ts deleted file mode 100644 index 531c1ad2..00000000 --- a/packages/cli/tests/webviewer-apps.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { TYPEGEN_VERSION } from "../src/package-versions.js"; - -const nonInteractiveDirectoryError = /already exists and isn't empty/; -const expectedTypegenVersion = `^${TYPEGEN_VERSION}`; - -describe("Web Viewer CLI Tests", () => { - const testDir = join(import.meta.dirname, "..", "..", "tmp", "cli-tests"); - const cliPath = join(import.meta.dirname, "..", "bin", "proofkit.cjs"); - const projectName = "test-webviewer-project"; - const projectDir = join(testDir, projectName); - - beforeEach(() => { - if (existsSync(projectDir)) { - rmSync(projectDir, { recursive: true, force: true }); - } - mkdirSync(testDir, { recursive: true }); - }); - - it("should create a webviewer project without FileMaker server setup", () => { - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(projectDir)).toBe(true); - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit-typegen.config.jsonc"))).toBe(true); - - const packageJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")); - expect(packageJson.scripts.typegen).toBe("pnpx @proofkit/typegen"); - expect(packageJson.scripts["typegen:ui"]).toBe("pnpx @proofkit/typegen ui"); - expect(packageJson.devDependencies["@proofkit/typegen"]).toBe(expectedTypegenVersion); - - const proofkitConfig = JSON.parse(readFileSync(join(projectDir, "proofkit.json"), "utf-8")); - expect(proofkitConfig.appType).toBe("webviewer"); - expect(proofkitConfig.dataSources).toEqual([]); - }); - - it("should allow agent-only folders in non-interactive mode", () => { - mkdirSync(projectDir, { recursive: true }); - mkdirSync(join(projectDir, ".cursor"), { recursive: true }); - writeFileSync(join(projectDir, ".cursor", "rules.mdc"), "placeholder"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, ".cursor"))).toBe(true); - expect(existsSync(join(projectDir, ".cursor", "rules"))).toBe(false); - }); - - it("should allow hidden files in non-interactive mode", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, ".DS_Store"), "placeholder"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, ".DS_Store"))).toBe(true); - }); - - it("should fail in non-interactive mode when .gitignore already exists", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, ".gitignore"), "node_modules/\n"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(nonInteractiveDirectoryError); - - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - }); - - it("should fail without prompting when a non-interactive target directory has real files", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, "README.md"), "existing content"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(nonInteractiveDirectoryError); - - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - }); -}); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json deleted file mode 100644 index 7f3eae14..00000000 --- a/packages/cli/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": "./", - "exactOptionalPropertyTypes": false, - "paths": { - "~/*": ["./src/*"] - }, - "strictNullChecks": true - }, - "exclude": ["template", "dist"], - "include": ["src", "tests", "tsdown.config.ts", "vitest.config.ts"] -} diff --git a/packages/cli/tsdown.config.ts b/packages/cli/tsdown.config.ts deleted file mode 100644 index 0cb69d19..00000000 --- a/packages/cli/tsdown.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "tsdown"; - -const isDev = process.env.npm_lifecycle_event === "dev"; - -export default defineConfig({ - clean: true, - entry: ["src/index.ts"], - format: ["esm"], - minify: !isDev, - target: "esnext", - outDir: "dist", - nodeProtocol: false, -}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts deleted file mode 100644 index dbff20f2..00000000 --- a/packages/cli/vitest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import path from "node:path"; -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(import.meta.dirname, "src"), - }, - }, - test: { - globals: true, - environment: "node", - include: ["tests/**/*.test.ts"], - exclude: ["**/node_modules/**", "**/dist/**", "tests/**/*.smoke.test.ts"], - fileParallelism: false, - testTimeout: 60_000, - }, -}); diff --git a/packages/cli/vitest.smoke.config.ts b/packages/cli/vitest.smoke.config.ts deleted file mode 100644 index 7a62c5b2..00000000 --- a/packages/cli/vitest.smoke.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vitest/config"; - -const configDir = path.dirname(fileURLToPath(import.meta.url)); - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(configDir, "src"), - }, - }, - test: { - globals: true, - environment: "node", - setupFiles: ["./tests/setup.ts"], - include: ["tests/**/*.smoke.test.ts"], - exclude: ["**/node_modules/**", "**/dist/**"], - testTimeout: 180_000, - }, -}); diff --git a/packages/create-proofkit/CHANGELOG.md b/packages/create-proofkit/CHANGELOG.md deleted file mode 100644 index bec232d4..00000000 --- a/packages/create-proofkit/CHANGELOG.md +++ /dev/null @@ -1,49 +0,0 @@ -# create-proofkit - -## 0.1.3 - -### Patch Changes - -- 7d48139: Ship ProofKit CLI in installer and publish CLI packages. - -## 0.1.3-beta.0 - -### Patch Changes - -- 7d48139: Ship ProofKit CLI in installer and publish CLI packages. - -## 0.1.2 - -### Patch Changes - -- b62a73c: Restrict Node engines to 22, 24, or 26. - -## 0.1.1 - -### Patch Changes - -- Maintenance release: internal tooling and docs updates. - -## 0.1.1-beta.1 - -### Patch Changes - -- 7c7f70a: swap docs domain to proofkit.proof.sh - -## 0.1.1-beta.0 - -### Patch Changes - -- 863e1e8: Update tooling to Biome - -## 0.1.0 - -### Minor Changes - -- c348e37: Support @proofkit namespaced packages - -## 0.1.0-beta.0 - -### Minor Changes - -- Support @proofkit namespaced packages diff --git a/packages/create-proofkit/README.md b/packages/create-proofkit/README.md deleted file mode 100644 index 02a7bded..00000000 --- a/packages/create-proofkit/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Create ProofKit - -```bash -npx create proofkit -``` - -This is a simple alias package for the ProofKit CLI. For full documentation, see the [ProofKit Docs](https://proofkit.proof.sh). diff --git a/packages/create-proofkit/package.json b/packages/create-proofkit/package.json deleted file mode 100644 index d090f644..00000000 --- a/packages/create-proofkit/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "create-proofkit", - "version": "0.1.3", - "description": "Create a new ProofKit project", - "type": "module", - "bin": "./src/index.js", - "repository": { - "type": "git", - "url": "https://github.com/proofsh/proofkit.git", - "directory": "packages/create-proofkit" - }, - "files": [ - "src" - ], - "scripts": { - "dev:watch": "node src/index.js", - "pub:release": "npm publish --access public", - "pub:beta": "npm publish --tag beta --access public", - "lint": "cd ../.. && pnpm exec ultracite check packages/create-proofkit/src packages/create-proofkit/tests packages/create-proofkit/package.json packages/create-proofkit/vitest.config.ts", - "lint:summary": "pnpm run lint", - "test": "vitest run --config vitest.config.ts" - }, - "dependencies": { - "execa": "^9.6.1" - }, - "devDependencies": { - "vitest": "^4.0.17" - }, - "engines": { - "node": "^22.0.0 || ^24.0.0" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/create-proofkit/src/getUserPkgManager.js b/packages/create-proofkit/src/getUserPkgManager.js deleted file mode 100644 index ef962926..00000000 --- a/packages/create-proofkit/src/getUserPkgManager.js +++ /dev/null @@ -1,22 +0,0 @@ -/** @typedef {"npm" | "pnpm" | "yarn" | "bun"} PackageManager */ - -/** @returns {PackageManager} */ -export const getUserPkgManager = () => { - // 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/create-proofkit/src/index.js b/packages/create-proofkit/src/index.js deleted file mode 100644 index 3fe032c0..00000000 --- a/packages/create-proofkit/src/index.js +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env node - -import { createRequire } from "node:module"; -import { execa } from "execa"; -import { getUserPkgManager } from "./getUserPkgManager.js"; - -const require = createRequire(import.meta.url); -const packageJson = require("../package.json"); - -function getCliSpecifier() { - const version = packageJson.version; - const tag = version.includes("-") ? "beta" : "latest"; - - return `@proofkit/cli@${tag}`; -} - -async function main() { - const args = process.argv.slice(2); - - const pkgManager = getUserPkgManager(); - let pkgManagerCmd; - if (pkgManager === "pnpm") { - pkgManagerCmd = "pnpx"; - } else if (pkgManager === "bun") { - pkgManagerCmd = "bunx"; - } else if (pkgManager === "npm") { - pkgManagerCmd = "npx"; - } else { - pkgManagerCmd = pkgManager; - } - - try { - await execa(pkgManagerCmd, [getCliSpecifier(), "init", ...args], { - stdio: "inherit", - env: { - ...process.env, - FORCE_COLOR: "1", // Preserve colors in output - }, - }); - } catch { - console.error("Failed to create project"); - process.exit(1); - } -} - -main().catch(() => { - console.error("Failed to create project"); - process.exit(1); -}); diff --git a/packages/create-proofkit/tests/index.test.js b/packages/create-proofkit/tests/index.test.js deleted file mode 100644 index 0b7da027..00000000 --- a/packages/create-proofkit/tests/index.test.js +++ /dev/null @@ -1,104 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import packageJson from "../package.json"; - -const { execaMock } = vi.hoisted(() => ({ - execaMock: vi.fn(), -})); - -vi.mock("execa", () => ({ - execa: execaMock, -})); -const originalArgv = [...process.argv]; -const expectedCliTag = packageJson.version.includes("-") ? "beta" : "latest"; - -let processExitSpy; -let consoleErrorSpy; - -const importWrapperEntry = async () => { - vi.resetModules(); - await import("../src/index.js"); - await Promise.resolve(); -}; - -describe("create-proofkit wrapper", () => { - beforeEach(() => { - execaMock.mockReset(); - execaMock.mockResolvedValue({}); - - processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined); - consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - }); - - afterEach(() => { - process.argv = [...originalArgv]; - vi.unstubAllEnvs(); - - vi.restoreAllMocks(); - }); - - it.each([ - ["npm", "npm/10.0.0 node/v22.0.0 darwin x64", "npx"], - ["pnpm", "pnpm/9.0.0 node/v22.0.0 darwin x64", "pnpx"], - ["yarn", "yarn/1.22.22 npm/? node/v22.0.0 darwin x64", "yarn"], - ["bun", "bun/1.1.0 node/v22.0.0 darwin x64", "bunx"], - ])("dispatches %s user agents to the expected command", async (_label, userAgent, expectedCommand) => { - vi.stubEnv("npm_config_user_agent", userAgent); - process.argv = ["node", "create-proofkit", "my-app"]; - - await importWrapperEntry(); - - expect(execaMock).toHaveBeenCalledWith( - expectedCommand, - [`@proofkit/cli@${expectedCliTag}`, "init", "my-app"], - expect.objectContaining({ - stdio: "inherit", - env: expect.objectContaining({ - FORCE_COLOR: "1", - }), - }), - ); - expect(processExitSpy).not.toHaveBeenCalled(); - }); - - it("forwards arbitrary init args unchanged", async () => { - const forwardedArgs = ["my-app", "--template", "next", "--install=false", "--yes"]; - vi.stubEnv("npm_config_user_agent", "npm/10.0.0 node/v22.0.0 darwin x64"); - process.argv = ["node", "create-proofkit", ...forwardedArgs]; - - await importWrapperEntry(); - - expect(execaMock).toHaveBeenCalledWith( - "npx", - [`@proofkit/cli@${expectedCliTag}`, "init", ...forwardedArgs], - expect.any(Object), - ); - expect(processExitSpy).not.toHaveBeenCalled(); - }); - - it("falls back to pnpm when no user agent is present", async () => { - vi.stubEnv("npm_config_user_agent", ""); - process.argv = ["node", "create-proofkit", "fallback-app"]; - - await importWrapperEntry(); - - expect(execaMock).toHaveBeenCalledWith( - "pnpx", - [`@proofkit/cli@${expectedCliTag}`, "init", "fallback-app"], - expect.any(Object), - ); - expect(processExitSpy).not.toHaveBeenCalled(); - }); - - it("prints an error and exits when the wrapper command fails", async () => { - execaMock.mockRejectedValueOnce(new Error("boom")); - vi.stubEnv("npm_config_user_agent", "npm/10.0.0 node/v22.0.0 darwin x64"); - process.argv = ["node", "create-proofkit", "broken-app"]; - - await importWrapperEntry(); - - await vi.waitFor(() => { - expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to create project"); - expect(processExitSpy).toHaveBeenCalledWith(1); - }); - }); -}); diff --git a/packages/create-proofkit/vitest.config.ts b/packages/create-proofkit/vitest.config.ts deleted file mode 100644 index f12e4e8a..00000000 --- a/packages/create-proofkit/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - environment: "node", - include: ["tests/**/*.test.js"], - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d483d16e..3b31fcbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -277,106 +277,6 @@ importers: specifier: ^4.0.17 version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(@vitest/ui@3.2.4)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) - packages/cli: - devDependencies: - '@clack/prompts': - specifier: ^0.11.0 - version: 0.11.0 - '@effect/cli': - specifier: 0.74.0 - version: 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/printer-ansi@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0))(@effect/printer@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/platform': - specifier: 0.95.0 - version: 0.95.0(effect@3.20.0) - '@effect/platform-node': - specifier: 0.105.0 - version: 0.105.0(@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/printer': - specifier: 0.48.0 - version: 0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0) - '@effect/printer-ansi': - specifier: 0.48.0 - version: 0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0) - '@inquirer/prompts': - specifier: ^8.3.2 - version: 8.3.2(@types/node@22.19.5) - '@proofkit/fmdapi': - specifier: workspace:* - version: link:../fmdapi - '@proofkit/typegen': - specifier: workspace:* - version: link:../typegen - '@types/fs-extra': - specifier: ^11.0.4 - version: 11.0.4 - '@types/gradient-string': - specifier: ^1.1.6 - version: 1.1.6 - '@types/node': - specifier: ^22.19.5 - version: 22.19.5 - '@types/randomstring': - specifier: ^1.3.0 - version: 1.3.0 - axios: - specifier: ^1.13.2 - version: 1.13.2 - chalk: - specifier: 5.4.1 - version: 5.4.1 - dotenv: - specifier: ^16.6.1 - version: 16.6.1 - effect: - specifier: ^3.20.0 - version: 3.20.0 - execa: - specifier: ^9.6.1 - version: 9.6.1 - fs-extra: - specifier: ^11.3.3 - version: 11.3.3 - gradient-string: - specifier: ^2.0.2 - version: 2.0.2 - jsonc-parser: - specifier: ^3.3.1 - version: 3.3.1 - open: - specifier: ^10.2.0 - version: 10.2.0 - publint: - specifier: ^0.3.16 - version: 0.3.16 - randomstring: - specifier: ^1.3.1 - version: 1.3.1 - tsdown: - specifier: ^0.14.2 - version: 0.14.2(oxc-resolver@11.16.2)(publint@0.3.16)(typescript@5.9.3) - type-fest: - specifier: ^3.13.1 - version: 3.13.1 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vitest: - specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) - zod: - specifier: ^4.3.5 - version: 4.3.5 - - packages/create-proofkit: - dependencies: - execa: - specifier: ^9.6.1 - version: 9.6.1 - devDependencies: - vitest: - specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(@vitest/ui@3.2.4)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) - packages/fmdapi: dependencies: '@standard-schema/spec': @@ -1287,97 +1187,6 @@ packages: peerDependencies: react: '>=16.8.0' - '@effect/cli@0.74.0': - resolution: {integrity: sha512-vjMJWJWQ2zMRVcZJj2ZGr7vFgVoX6lsCuqAsNiN2ndWZAidkEJ6g1Euuib2V2nTXeWvRyd3FY2Fw2UvX48Uenw==} - peerDependencies: - '@effect/platform': ^0.95.0 - '@effect/printer': ^0.48.0 - '@effect/printer-ansi': ^0.48.0 - effect: ^3.20.0 - - '@effect/cluster@0.57.0': - resolution: {integrity: sha512-VjZoZ4hmgDb0GtGjktypTk/nArA3ntsXU2O9vOBzDjJLRKVBt7IS0/cllHrHwK5Jxkfz86B2k+Prw4/+nrLFlw==} - peerDependencies: - '@effect/platform': ^0.95.0 - '@effect/rpc': ^0.74.0 - '@effect/sql': ^0.50.0 - '@effect/workflow': ^0.17.0 - effect: ^3.20.0 - - '@effect/experimental@0.59.0': - resolution: {integrity: sha512-XqdBpIH5VLlkRxKlyPYp8TAYUeBPjoWYgtrxDebDab14K4kkrpkHk0ZsmmOiQUZ+LY5veRn/PBSogXor9gtPqg==} - peerDependencies: - '@effect/platform': ^0.95.0 - effect: ^3.20.0 - ioredis: ^5 - lmdb: ^3 - peerDependenciesMeta: - ioredis: - optional: true - lmdb: - optional: true - - '@effect/platform-node-shared@0.58.0': - resolution: {integrity: sha512-kl8ejYM1xvjRlk+4/R1YzB6A3E3hVWY4jIfEl21uu4S43V0S15gHvcur7iMIEXfJTX1a25EKF+Buef+Yv5wZZQ==} - peerDependencies: - '@effect/cluster': ^0.57.0 - '@effect/platform': ^0.95.0 - '@effect/rpc': ^0.74.0 - '@effect/sql': ^0.50.0 - effect: ^3.20.0 - - '@effect/platform-node@0.105.0': - resolution: {integrity: sha512-6JxOLqLJMm+m1ZQavIb75S7YJ4fRvrDaYUZ4rqv2IMq5ZK9HVaU/LeejE9tip9zAG9yNM/6mn183iiIV/xge5w==} - peerDependencies: - '@effect/cluster': ^0.57.0 - '@effect/platform': ^0.95.0 - '@effect/rpc': ^0.74.0 - '@effect/sql': ^0.50.0 - effect: ^3.20.0 - - '@effect/platform@0.95.0': - resolution: {integrity: sha512-WDlRiWRSWlmhCPq09bvAofK0qr5vM4yNklXjoJdZHmugKRRTpN/Okn3ODnjgM/Kb/4hjMrRyrsUeH/Brieq7KA==} - peerDependencies: - effect: ^3.20.0 - - '@effect/printer-ansi@0.48.0': - resolution: {integrity: sha512-CzQ5kiomjR9DZ6LPfKAaWmys6JU65c2Q/VQcTKRK4RfaDWeTAehpAVmgOIyKSPkcr9XBhjo2cJx4xyZ4E5nN7g==} - peerDependencies: - '@effect/typeclass': ^0.39.0 - effect: ^3.20.0 - - '@effect/printer@0.48.0': - resolution: {integrity: sha512-f/+QVyqACuLkoB+HDDX2XxloslmgMDL+C6ecHBV0cB0zJzJmLCOybwOkRcCI2xJ/DWHEIpoRyvq+Bfdza0AIrA==} - peerDependencies: - '@effect/typeclass': ^0.39.0 - effect: ^3.20.0 - - '@effect/rpc@0.74.0': - resolution: {integrity: sha512-EV/cHQqJxLtY+RTlPlVQU1KyTzml1wFne+Sh91RacGRRVh6uTm4UdhRh9TNtbYHD4rM9yD3T6zqUgKr0AH8MvQ==} - peerDependencies: - '@effect/platform': ^0.95.0 - effect: ^3.20.0 - - '@effect/sql@0.50.0': - resolution: {integrity: sha512-sOTzsC+ICASgSmX1RITYo6ut7ZbkX+hMG6YagJEyhtptxco9MgSflpF/ix/L92haJ+YTS5Zur/Dm2bDNfVes4w==} - peerDependencies: - '@effect/experimental': ^0.59.0 - '@effect/platform': ^0.95.0 - effect: ^3.20.0 - - '@effect/typeclass@0.39.0': - resolution: {integrity: sha512-V8qGpm4BTMS4pW9e7aCdxC0sy/TYsdxmnpWtokkNWnggZ6kvh1Psp3AfUuuZLyNmUk4T+lYB/ItEsga/+hryig==} - peerDependencies: - effect: ^3.20.0 - - '@effect/workflow@0.17.0': - resolution: {integrity: sha512-JiayvFTTMrp36P0cVFcgu6Nb7ZJxQv+FRqs3DPORkVAcCZlWOKa3KyuYebN3qZbRsmLzS7cxuC8BAeMuqb+WaQ==} - peerDependencies: - '@effect/experimental': ^0.59.0 - '@effect/platform': ^0.95.0 - '@effect/rpc': ^0.74.0 - effect: ^3.20.0 - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -2088,19 +1897,6 @@ packages: resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} - '@inquirer/ansi@2.0.4': - resolution: {integrity: sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - - '@inquirer/checkbox@5.1.2': - resolution: {integrity: sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/confirm@5.1.21': resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} @@ -2110,15 +1906,6 @@ packages: '@types/node': optional: true - '@inquirer/confirm@6.0.10': - resolution: {integrity: sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/core@10.3.2': resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} @@ -2128,33 +1915,6 @@ packages: '@types/node': optional: true - '@inquirer/core@11.1.7': - resolution: {integrity: sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/editor@5.0.10': - resolution: {integrity: sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/expand@5.0.10': - resolution: {integrity: sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -2164,86 +1924,10 @@ packages: '@types/node': optional: true - '@inquirer/external-editor@2.0.4': - resolution: {integrity: sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/figures@1.0.15': resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} - '@inquirer/figures@2.0.4': - resolution: {integrity: sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - - '@inquirer/input@5.0.10': - resolution: {integrity: sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/number@4.0.10': - resolution: {integrity: sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/password@5.0.10': - resolution: {integrity: sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/prompts@8.3.2': - resolution: {integrity: sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/rawlist@5.2.6': - resolution: {integrity: sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/search@4.1.6': - resolution: {integrity: sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/select@5.1.2': - resolution: {integrity: sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/type@3.0.10': resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} @@ -2253,15 +1937,6 @@ packages: '@types/node': optional: true - '@inquirer/type@4.0.4': - resolution: {integrity: sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -2366,36 +2041,6 @@ packages: '@mongodb-js/saslprep@1.4.6': resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - '@mswjs/interceptors@0.40.0': resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} engines: {node: '>=18'} @@ -2773,94 +2418,6 @@ packages: cpu: [x64] os: [win32] - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -4283,9 +3840,6 @@ packages: '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} - '@types/gradient-string@1.1.6': - resolution: {integrity: sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -4334,9 +3888,6 @@ packages: '@types/prompts@2.4.9': resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} - '@types/randomstring@1.3.0': - resolution: {integrity: sha512-kCP61wludjY7oNUeFiMxfswHB3Wn/aC03Cu82oQsNTO6OCuhVN/rCbBs68Cq6Nkgjmp2Sh3Js6HearJPkk7KQA==} - '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -4354,9 +3905,6 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - '@types/tinycolor2@1.4.6': - resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -4640,12 +4188,6 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - axios@1.13.2: - resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} - bail@1.0.5: resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} @@ -4988,10 +4530,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -5134,11 +4672,7 @@ packages: engines: {node: '>=12'} defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} @@ -5285,10 +4819,6 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - es-toolkit@1.43.0: resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} @@ -5434,18 +4964,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} - - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-wrap-ansi@0.2.0: - resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - fast-xml-parser@5.3.3: resolution: {integrity: sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA==} hasBin: true @@ -5493,9 +5014,6 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -5517,23 +5035,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -5757,10 +5262,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - gradient-string@2.0.2: - resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==} - engines: {node: '>=10'} - graphql@16.12.0: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -5781,10 +5282,6 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hashery@1.5.0: resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} engines: {node: '>=20'} @@ -5900,10 +5397,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@4.1.3: - resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -6168,9 +5661,6 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - kubernetes-types@1.30.0: - resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} - kysely@0.28.12: resolution: {integrity: sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw==} engines: {node: '>=20.0.0'} @@ -6616,27 +6106,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -6728,13 +6205,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - - msgpackr@1.11.9: - resolution: {integrity: sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==} - msw@2.12.7: resolution: {integrity: sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==} engines: {node: '>=18'} @@ -6748,17 +6218,10 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} - mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - mute-stream@3.0.0: - resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} - engines: {node: ^20.17.0 || >=22.9.0} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -6831,9 +6294,6 @@ packages: sass: optional: true - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -6859,10 +6319,6 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -7173,9 +6629,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -7224,13 +6677,6 @@ packages: '@types/react-dom': optional: true - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - randomstring@1.3.1: - resolution: {integrity: sha512-lgXZa80MUkjWdE7g2+PZ1xDLzc7/RokXVEQOv5NN2UOTChW1I8A9gha5a9xYBOqgaSoI6uJikDmCU8PyRdArRQ==} - hasBin: true - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -7845,9 +7291,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -7856,9 +7299,6 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinygradient@1.1.5: - resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} - tinyrainbow@1.2.0: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} @@ -7886,9 +7326,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -8085,10 +7522,6 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.24.4: - resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} - engines: {node: '>=20.18.1'} - unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} @@ -9115,102 +8548,6 @@ snapshots: react: 19.2.3 tslib: 2.8.1 - '@effect/cli@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/printer-ansi@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0))(@effect/printer@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/printer': 0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0) - '@effect/printer-ansi': 0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 - ini: 4.1.3 - toml: 3.0.0 - yaml: 2.8.2 - - '@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/rpc': 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/workflow': 0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 - kubernetes-types: 1.30.0 - - '@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/platform': 0.95.0(effect@3.20.0) - effect: 3.20.0 - uuid: 11.1.0 - - '@effect/platform-node-shared@0.58.0(@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/cluster': 0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/rpc': 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@parcel/watcher': 2.5.6 - effect: 3.20.0 - multipasta: 0.2.7 - ws: 8.19.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform-node@0.105.0(@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/cluster': 0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/platform-node-shared': 0.58.0(@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/rpc': 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 - mime: 3.0.0 - undici: 7.24.4 - ws: 8.19.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform@0.95.0(effect@3.20.0)': - dependencies: - effect: 3.20.0 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.9 - multipasta: 0.2.7 - - '@effect/printer-ansi@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/printer': 0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0) - '@effect/typeclass': 0.39.0(effect@3.20.0) - effect: 3.20.0 - - '@effect/printer@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/typeclass': 0.39.0(effect@3.20.0) - effect: 3.20.0 - - '@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/platform': 0.95.0(effect@3.20.0) - effect: 3.20.0 - msgpackr: 1.11.9 - - '@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/experimental': 0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.95.0(effect@3.20.0) - effect: 3.20.0 - uuid: 11.1.0 - - '@effect/typeclass@0.39.0(effect@3.20.0)': - dependencies: - effect: 3.20.0 - - '@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/experimental': 0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/rpc': 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -9650,17 +8987,6 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/ansi@2.0.4': {} - - '@inquirer/checkbox@5.1.2(@types/node@22.19.5)': - dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/confirm@5.1.21(@types/node@22.19.5)': dependencies: '@inquirer/core': 10.3.2(@types/node@22.19.5) @@ -9676,13 +9002,6 @@ snapshots: '@types/node': 25.0.6 optional: true - '@inquirer/confirm@6.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/core@10.3.2(@types/node@22.19.5)': dependencies: '@inquirer/ansi': 1.0.2 @@ -9710,33 +9029,6 @@ snapshots: '@types/node': 25.0.6 optional: true - '@inquirer/core@11.1.7(@types/node@22.19.5)': - dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@22.19.5) - cli-width: 4.1.0 - fast-wrap-ansi: 0.2.0 - mute-stream: 3.0.0 - signal-exit: 4.1.0 - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/editor@5.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/external-editor': 2.0.4(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/expand@5.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/external-editor@1.0.3(@types/node@22.19.5)': dependencies: chardet: 2.1.1 @@ -9744,78 +9036,8 @@ snapshots: optionalDependencies: '@types/node': 22.19.5 - '@inquirer/external-editor@2.0.4(@types/node@22.19.5)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/figures@1.0.15': {} - '@inquirer/figures@2.0.4': {} - - '@inquirer/input@5.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/number@4.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/password@5.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/prompts@8.3.2(@types/node@22.19.5)': - dependencies: - '@inquirer/checkbox': 5.1.2(@types/node@22.19.5) - '@inquirer/confirm': 6.0.10(@types/node@22.19.5) - '@inquirer/editor': 5.0.10(@types/node@22.19.5) - '@inquirer/expand': 5.0.10(@types/node@22.19.5) - '@inquirer/input': 5.0.10(@types/node@22.19.5) - '@inquirer/number': 4.0.10(@types/node@22.19.5) - '@inquirer/password': 5.0.10(@types/node@22.19.5) - '@inquirer/rawlist': 5.2.6(@types/node@22.19.5) - '@inquirer/search': 4.1.6(@types/node@22.19.5) - '@inquirer/select': 5.1.2(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/rawlist@5.2.6(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/search@4.1.6(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/select@5.1.2(@types/node@22.19.5)': - dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/type@3.0.10(@types/node@22.19.5)': optionalDependencies: '@types/node': 22.19.5 @@ -9825,10 +9047,6 @@ snapshots: '@types/node': 25.0.6 optional: true - '@inquirer/type@4.0.4(@types/node@22.19.5)': - optionalDependencies: - '@types/node': 22.19.5 - '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -10063,24 +9281,6 @@ snapshots: dependencies: sparse-bitfield: 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true - '@mswjs/interceptors@0.40.0': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -10343,66 +9543,6 @@ snapshots: '@oxlint/win32-x64@1.39.0': optional: true - '@parcel/watcher-android-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-x64@2.5.6': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.6': - optional: true - - '@parcel/watcher-win32-arm64@2.5.6': - optional: true - - '@parcel/watcher-win32-ia32@2.5.6': - optional: true - - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': - dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.3 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 - '@polka/url@1.0.0-next.29': {} '@posthog/core@1.28.3': @@ -11897,10 +11037,6 @@ snapshots: '@types/jsonfile': 6.1.4 '@types/node': 25.0.6 - '@types/gradient-string@1.1.6': - dependencies: - '@types/tinycolor2': 1.4.6 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -11957,8 +11093,6 @@ snapshots: '@types/node': 25.0.6 kleur: 3.0.3 - '@types/randomstring@1.3.0': {} - '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -11973,8 +11107,6 @@ snapshots: '@types/statuses@2.0.6': {} - '@types/tinycolor2@1.4.6': {} - '@types/trusted-types@2.0.7': optional: true @@ -12091,14 +11223,14 @@ snapshots: msw: 2.12.7(@types/node@22.19.5)(typescript@5.9.3) vite: 6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@25.0.6)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.7(@types/node@25.0.6)(typescript@5.9.3) - vite: 6.4.1(@types/node@25.0.6)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': dependencies: @@ -12328,16 +11460,6 @@ snapshots: astring@1.9.0: {} - asynckit@0.4.0: {} - - axios@1.13.2: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - bail@1.0.5: {} bail@2.0.2: {} @@ -12665,10 +11787,6 @@ snapshots: colorette@2.0.20: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - comma-separated-tokens@2.0.3: {} commander@10.0.1: {} @@ -12780,8 +11898,6 @@ snapshots: defu@6.1.4: {} - delayed-stream@1.0.0: {} - depd@2.0.0: {} dequal@2.0.3: {} @@ -12885,13 +12001,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - es-toolkit@1.43.0: {} esast-util-from-estree@2.0.0: @@ -13156,18 +12265,8 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-string-truncated-width@3.0.3: {} - - fast-string-width@3.0.2: - dependencies: - fast-string-truncated-width: 3.0.3 - fast-uri@3.1.0: {} - fast-wrap-ansi@0.2.0: - dependencies: - fast-string-width: 3.0.2 - fast-xml-parser@5.3.3: dependencies: strnum: 2.1.2 @@ -13220,8 +12319,6 @@ snapshots: transitivePeerDependencies: - supports-color - find-my-way-ts@0.1.6: {} - find-up-simple@1.0.1: {} find-up@3.0.0: @@ -13243,21 +12340,11 @@ snapshots: flatted@3.4.2: {} - follow-redirects@1.15.11: {} - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - format@0.2.2: {} formatly@0.3.0: @@ -13541,11 +12628,6 @@ snapshots: graceful-fs@4.2.11: {} - gradient-string@2.0.2: - dependencies: - chalk: 4.1.2 - tinygradient: 1.1.5 - graphql@16.12.0: {} happy-dom@20.1.0: @@ -13565,10 +12647,6 @@ snapshots: has-symbols@1.1.0: {} - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - hashery@1.5.0: dependencies: hookified: 1.15.1 @@ -13708,8 +12786,6 @@ snapshots: inherits@2.0.4: {} - ini@4.1.3: {} - inline-style-parser@0.2.7: {} ipaddr.js@1.9.1: {} @@ -13947,8 +13023,6 @@ snapshots: kolorist@1.8.0: {} - kubernetes-types@1.30.0: {} - kysely@0.28.12: {} levn@0.4.1: @@ -14707,20 +13781,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - mime-types@3.0.2: dependencies: mime-db: 1.54.0 - mime@3.0.0: {} - mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -14781,22 +13847,6 @@ snapshots: ms@2.1.3: {} - msgpackr-extract@3.0.3: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - optional: true - - msgpackr@1.11.9: - optionalDependencies: - msgpackr-extract: 3.0.3 - msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3): dependencies: '@inquirer/confirm': 5.1.21(@types/node@22.19.5) @@ -14850,12 +13900,8 @@ snapshots: muggle-string@0.4.1: {} - multipasta@0.2.7: {} - mute-stream@2.0.0: {} - mute-stream@3.0.0: {} - mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -14929,8 +13975,6 @@ snapshots: - babel-plugin-macros - varlock - node-addon-api@7.1.1: {} - node-domexception@1.0.0: {} node-emoji@2.2.0: @@ -14952,11 +13996,6 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - node-releases@2.0.27: {} nodemon@3.1.11: @@ -15316,8 +14355,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} - pstree.remy@1.1.8: {} publint@0.3.16: @@ -15410,14 +14447,6 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - randomstring@1.3.1: - dependencies: - randombytes: 2.1.0 - range-parser@1.2.1: {} raw-body@3.0.2: @@ -16226,8 +15255,6 @@ snapshots: tinybench@2.9.0: {} - tinycolor2@1.6.0: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -16235,11 +15262,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinygradient@1.1.5: - dependencies: - '@types/tinycolor2': 1.4.6 - tinycolor2: 1.6.0 - tinyrainbow@1.2.0: {} tinyrainbow@2.0.0: {} @@ -16258,8 +15280,6 @@ snapshots: toidentifier@1.0.1: {} - toml@3.0.0: {} - totalist@3.0.1: {} touch@3.1.1: {} @@ -16442,8 +15462,6 @@ snapshots: undici-types@7.16.0: {} - undici@7.24.4: {} - unfetch@4.2.0: {} unicode-emoji-modifier-base@1.0.0: {} @@ -16869,7 +15887,7 @@ snapshots: vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@25.0.6)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.17 '@vitest/runner': 4.0.17 '@vitest/snapshot': 4.0.17

@gF3vyIQ3^F;$1B5tV{7qWSmqyf&_UlPLrKfV@ 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≧=0}M4k$sCjb)kjgWwMt9V(e(ofO>_q#wvO4kvHV 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