From a4443ea8395b4a8785d6479fdcf3c3afbbb782e0 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:57:15 -0500 Subject: [PATCH 01/14] Add direct wrapper tests for create-proofkit Agent-Id: agent-b0866404-cae3-459d-a9c5-f749470d81b4 --- .changeset/proofkit-cli-major-migration.md | 5 + .github/workflows/continuous-release.yml | 2 +- .github/workflows/release.yml | 5 +- packages/cli-old/.yarnrc.yml | 5 + packages/cli-old/CHANGELOG.md | 285 ++++++ packages/cli-old/README.md | 19 + packages/cli-old/index.d.ts | 19 + packages/cli-old/package.json | 128 +++ .../{cli => cli-old}/proofkit-cli-1.1.8.tgz | Bin packages/cli-old/src/cli/add/auth.ts | 110 +++ .../cli/add/data-source/deploy-demo-file.ts | 96 ++ .../src/cli/add/data-source/filemaker.ts | 441 +++++++++ .../cli-old/src/cli/add/data-source/index.ts | 46 + packages/cli-old/src/cli/add/fmschema.ts | 216 +++++ packages/cli-old/src/cli/add/index.ts | 190 ++++ packages/cli-old/src/cli/add/page/index.ts | 230 +++++ .../add/page/post-install/table-infinite.ts | 12 + .../src/cli/add/page/post-install/table.ts | 123 +++ .../cli-old/src/cli/add/page/templates.ts | 85 ++ packages/cli-old/src/cli/add/page/types.ts | 19 + .../src/cli/add/registry/getOptions.ts | 44 + packages/cli-old/src/cli/add/registry/http.ts | 18 + .../cli-old/src/cli/add/registry/install.ts | 224 +++++ .../cli-old/src/cli/add/registry/listItems.ts | 9 + .../add/registry/postInstall/handlebars.ts | 189 ++++ .../src/cli/add/registry/postInstall/index.ts | 22 + .../registry/postInstall/package-script.ts | 12 + .../add/registry/postInstall/wrap-provider.ts | 132 +++ .../cli-old/src/cli/add/registry/preflight.ts | 17 + packages/cli-old/src/cli/deploy/index.ts | 489 ++++++++++ packages/cli-old/src/cli/fmdapi.ts | 57 ++ packages/cli-old/src/cli/init.ts | 395 ++++++++ packages/cli-old/src/cli/menu.ts | 102 ++ packages/cli-old/src/cli/ottofms.ts | 268 ++++++ packages/cli-old/src/cli/prompts.ts | 188 ++++ packages/cli-old/src/cli/react-email.ts | 27 + .../cli-old/src/cli/remove/data-source.ts | 153 +++ packages/cli-old/src/cli/remove/index.ts | 72 ++ packages/cli-old/src/cli/remove/page.ts | 214 +++++ packages/cli-old/src/cli/remove/schema.ts | 100 ++ packages/cli-old/src/cli/tanstack-query.ts | 19 + packages/cli-old/src/cli/typegen/index.ts | 20 + packages/cli-old/src/cli/update/index.ts | 28 + .../src/cli/update/makeUpgradeCommand.ts | 25 + packages/cli-old/src/cli/utils.ts | 49 + packages/cli-old/src/consts.ts | 35 + packages/cli-old/src/generators/auth.ts | 83 ++ packages/cli-old/src/generators/fmdapi.ts | 525 ++++++++++ packages/cli-old/src/generators/route.ts | 40 + .../cli-old/src/generators/tanstack-query.ts | 97 ++ packages/cli-old/src/globalOptions.ts | 8 + packages/cli-old/src/globals.d.ts | 4 + packages/cli-old/src/helpers/createProject.ts | 129 +++ packages/cli-old/src/helpers/fmHttp.ts | 56 ++ packages/cli-old/src/helpers/git.ts | 140 +++ .../src/helpers/installDependencies.ts | 242 +++++ .../cli-old/src/helpers/installPackages.ts | 25 + packages/cli-old/src/helpers/logNextSteps.ts | 48 + packages/cli-old/src/helpers/replaceText.ts | 17 + .../cli-old/src/helpers/scaffoldProject.ts | 136 +++ .../cli-old/src/helpers/selectBoilerplate.ts | 32 + .../cli-old/src/helpers/setImportAlias.ts | 12 + packages/cli-old/src/helpers/shadcn-cli.ts | 80 ++ packages/cli-old/src/helpers/stealth-init.ts | 20 + .../cli-old/src/helpers/version-fetcher.ts | 131 +++ packages/cli-old/src/index.ts | 96 ++ .../cli-old/src/installers/auth-shared.ts | 49 + .../cli-old/src/installers/better-auth.ts | 3 + packages/cli-old/src/installers/clerk.ts | 153 +++ .../src/installers/dependencyVersionMap.ts | 108 +++ packages/cli-old/src/installers/envVars.ts | 43 + packages/cli-old/src/installers/index.ts | 31 + .../src/installers/install-fm-addon.ts | 53 ++ packages/cli-old/src/installers/nextAuth.ts | 189 ++++ .../cli-old/src/installers/proofkit-auth.ts | 220 +++++ .../src/installers/proofkit-webviewer.ts | 84 ++ .../cli-old/src/installers/react-email.ts | 211 +++++ packages/cli-old/src/state.ts | 33 + packages/cli-old/src/upgrades/cursorRules.ts | 41 + packages/cli-old/src/upgrades/index.ts | 69 ++ packages/cli-old/src/upgrades/shadcn.ts | 53 ++ .../cli-old/src/utils/addPackageDependency.ts | 32 + packages/cli-old/src/utils/addToEnvs.ts | 131 +++ packages/cli-old/src/utils/formatting.ts | 24 + .../cli-old/src/utils/getProofKitVersion.ts | 38 + .../cli-old/src/utils/getUserPkgManager.ts | 21 + packages/cli-old/src/utils/isTTYError.ts | 1 + packages/cli-old/src/utils/logger.ts | 19 + .../cli-old/src/utils/parseNameAndPath.ts | 42 + packages/cli-old/src/utils/parseSettings.ts | 153 +++ .../src/utils/proofkitReleaseChannel.ts | 93 ++ .../cli-old/src/utils/removeTrailingSlash.ts | 6 + packages/cli-old/src/utils/renderTitle.ts | 20 + .../cli-old/src/utils/renderVersionWarning.ts | 86 ++ packages/cli-old/src/utils/ts-morph.ts | 25 + packages/cli-old/src/utils/validateAppName.ts | 22 + .../cli-old/src/utils/validateImportAlias.ts | 6 + .../conditional-rules/nextjs-framework.mdc | 51 + .../extras/_cursor/conditional-rules/npm.mdc | 60 ++ .../extras/_cursor/conditional-rules/pnpm.mdc | 65 ++ .../extras/_cursor/conditional-rules/yarn.mdc | 60 ++ .../extras/_cursor/rules/cursor-rules.mdc | 88 ++ .../extras/_cursor/rules/filemaker-api.mdc | 176 ++++ .../rules/troubleshooting-patterns.mdc | 240 +++++ .../extras/_cursor/rules/ui-components.mdc | 57 ++ .../extras/config/drizzle-config-mysql.ts | 12 + .../extras/config/drizzle-config-postgres.ts | 12 + .../extras/config/drizzle-config-sqlite.ts | 12 + .../extras/config/fmschema.config.mjs | 9 + .../extras/config/get-query-client.ts | 6 + .../template/extras/config/postcss.config.cjs | 7 + .../extras/config/query-provider-vite.tsx | 17 + .../template/extras/config/query-provider.tsx | 21 + .../extras/emailProviders/none/email.tsx | 24 + .../extras/emailProviders/plunk/email.tsx | 27 + .../extras/emailProviders/plunk/service.ts | 4 + .../extras/emailProviders/resend/email.tsx | 24 + .../extras/emailProviders/resend/service.ts | 4 + .../extras/emailTemplates/auth-code.tsx | 137 +++ .../extras/emailTemplates/generic.tsx | 113 +++ .../app/(main)/auth/profile/actions.ts | 97 ++ .../app/(main)/auth/profile/page.tsx | 29 + .../app/(main)/auth/profile/profile-form.tsx | 58 ++ .../auth/profile/reset-password-form.tsx | 112 +++ .../app/(main)/auth/profile/schema.ts | 19 + .../app/auth/forgot-password/actions.ts | 39 + .../app/auth/forgot-password/forgot-form.tsx | 42 + .../app/auth/forgot-password/page.tsx | 22 + .../app/auth/forgot-password/schema.ts | 5 + .../fmaddon-auth/app/auth/login/actions.ts | 35 + .../app/auth/login/login-form.tsx | 66 ++ .../fmaddon-auth/app/auth/login/page.tsx | 27 + .../fmaddon-auth/app/auth/login/schema.ts | 6 + .../app/auth/reset-password/actions.ts | 53 ++ .../app/auth/reset-password/page.tsx | 33 + .../reset-password/reset-password-form.tsx | 60 ++ .../app/auth/reset-password/schema.ts | 14 + .../reset-password/verify-email/actions.ts | 46 + .../auth/reset-password/verify-email/page.tsx | 33 + .../reset-password/verify-email/schema.ts | 5 + .../verify-email/verify-email-form.tsx | 49 + .../fmaddon-auth/app/auth/signup/actions.ts | 50 + .../fmaddon-auth/app/auth/signup/page.tsx | 27 + .../fmaddon-auth/app/auth/signup/schema.ts | 12 + .../app/auth/signup/signup-form.tsx | 68 ++ .../app/auth/verify-email/actions.ts | 109 +++ .../verify-email/email-verification-form.tsx | 46 + .../app/auth/verify-email/page.tsx | 40 + .../app/auth/verify-email/resend-button.tsx | 37 + .../app/auth/verify-email/schema.ts | 5 + .../fmaddon-auth/components/auth/actions.ts | 19 + .../fmaddon-auth/components/auth/protect.tsx | 18 + .../fmaddon-auth/components/auth/redirect.tsx | 26 + .../fmaddon-auth/components/auth/use-user.ts | 60 ++ .../components/auth/user-menu.tsx | 52 + .../extras/fmaddon-auth/emails/auth-code.tsx | 137 +++ .../extras/fmaddon-auth/middleware.ts | 44 + .../server/auth/utils/email-verification.ts | 137 +++ .../server/auth/utils/encryption.ts | 51 + .../fmaddon-auth/server/auth/utils/index.ts | 16 + .../server/auth/utils/password-reset.ts | 153 +++ .../server/auth/utils/password.ts | 67 ++ .../server/auth/utils/redirect.ts | 8 + .../fmaddon-auth/server/auth/utils/session.ts | 191 ++++ .../fmaddon-auth/server/auth/utils/user.ts | 146 +++ .../prisma/schema/base-planetscale.prisma | 24 + .../template/extras/prisma/schema/base.prisma | 20 + .../schema/with-auth-planetscale.prisma | 77 ++ .../extras/prisma/schema/with-auth.prisma | 74 ++ .../extras/src/app/_components/post-tw.tsx | 50 + .../extras/src/app/_components/post.tsx | 54 ++ .../src/app/api/auth/[...nextauth]/route.ts | 4 + .../extras/src/app/api/trpc/[trpc]/route.ts | 34 + .../extras/src/app/clerk-auth/layout.tsx | 10 + .../clerk-auth/signin/[[...sign-in]]/page.tsx | 5 + .../clerk-auth/signup/[[...sign-up]]/page.tsx | 5 + .../template/extras/src/app/layout/base.tsx | 34 + .../extras/src/app/layout/main-shell.tsx | 37 + .../extras/src/app/layout/with-trpc-tw.tsx | 24 + .../extras/src/app/layout/with-trpc.tsx | 24 + .../extras/src/app/layout/with-tw.tsx | 20 + .../extras/src/app/next-auth/layout.tsx | 22 + .../extras/src/app/next-auth/signin/page.tsx | 83 ++ .../extras/src/app/next-auth/signup/action.ts | 24 + .../extras/src/app/next-auth/signup/page.tsx | 40 + .../src/app/next-auth/signup/validation.ts | 12 + .../template/extras/src/app/page/base.tsx | 6 + .../extras/src/app/page/with-auth-trpc-tw.tsx | 67 ++ .../extras/src/app/page/with-auth-trpc.tsx | 68 ++ .../extras/src/app/page/with-trpc-tw.tsx | 53 ++ .../extras/src/app/page/with-trpc.tsx | 54 ++ .../template/extras/src/app/page/with-tw.tsx | 37 + .../components/clerk-auth/clerk-provider.tsx | 18 + .../clerk-auth/user-menu-mobile.tsx | 36 + .../src/components/clerk-auth/user-menu.tsx | 24 + .../next-auth/next-auth-provider.tsx | 14 + .../components/next-auth/user-menu-mobile.tsx | 31 + .../src/components/next-auth/user-menu.tsx | 38 + .../template/extras/src/env/with-auth.ts | 31 + .../template/extras/src/env/with-clerk.ts | 20 + .../template/extras/src/index.module.css | 177 ++++ .../template/extras/src/middleware/clerk.ts | 20 + .../extras/src/middleware/next-auth.ts | 5 + .../template/extras/src/pages/_app/base.tsx | 14 + .../src/pages/_app/with-auth-trpc-tw.tsx | 23 + .../extras/src/pages/_app/with-auth-trpc.tsx | 23 + .../extras/src/pages/_app/with-auth-tw.tsx | 21 + .../extras/src/pages/_app/with-auth.tsx | 21 + .../extras/src/pages/_app/with-trpc-tw.tsx | 16 + .../extras/src/pages/_app/with-trpc.tsx | 16 + .../extras/src/pages/_app/with-tw.tsx | 14 + .../src/pages/api/auth/[...nextauth].ts | 5 + .../extras/src/pages/api/trpc/[trpc].ts | 19 + .../template/extras/src/pages/index/base.tsx | 47 + .../src/pages/index/with-auth-trpc-tw.tsx | 80 ++ .../extras/src/pages/index/with-auth-trpc.tsx | 81 ++ .../extras/src/pages/index/with-trpc-tw.tsx | 52 + .../extras/src/pages/index/with-trpc.tsx | 53 ++ .../extras/src/pages/index/with-tw.tsx | 45 + .../template/extras/src/server/api/root.ts | 23 + .../src/server/api/routers/post/base.ts | 40 + .../api/routers/post/with-auth-drizzle.ts | 39 + .../api/routers/post/with-auth-prisma.ts | 41 + .../src/server/api/routers/post/with-auth.ts | 37 + .../server/api/routers/post/with-drizzle.ts | 30 + .../server/api/routers/post/with-prisma.ts | 31 + .../extras/src/server/api/trpc-app/base.ts | 103 ++ .../src/server/api/trpc-app/with-auth-db.ts | 133 +++ .../src/server/api/trpc-app/with-auth.ts | 130 +++ .../extras/src/server/api/trpc-app/with-db.ts | 106 +++ .../extras/src/server/api/trpc-pages/base.ts | 122 +++ .../src/server/api/trpc-pages/with-auth-db.ts | 160 ++++ .../src/server/api/trpc-pages/with-auth.ts | 158 +++ .../src/server/api/trpc-pages/with-db.ts | 125 +++ .../template/extras/src/server/data/users.ts | 23 + .../src/server/db/db-prisma-planetscale.ts | 22 + .../extras/src/server/db/db-prisma.ts | 17 + .../src/server/db/index-drizzle/with-mysql.ts | 18 + .../db/index-drizzle/with-planetscale.ts | 7 + .../server/db/index-drizzle/with-postgres.ts | 18 + .../server/db/index-drizzle/with-sqlite.ts | 19 + .../server/db/schema-drizzle/base-mysql.ts | 34 + .../db/schema-drizzle/base-planetscale.ts | 34 + .../server/db/schema-drizzle/base-postgres.ts | 36 + .../server/db/schema-drizzle/base-sqlite.ts | 30 + .../db/schema-drizzle/with-auth-mysql.ts | 123 +++ .../schema-drizzle/with-auth-planetscale.ts | 117 +++ .../db/schema-drizzle/with-auth-postgres.ts | 130 +++ .../db/schema-drizzle/with-auth-sqlite.ts | 116 +++ .../extras/src/server/next-auth/base.ts | 111 +++ .../extras/src/server/next-auth/password.ts | 13 + .../src/server/next-auth/with-drizzle.ts | 83 ++ .../src/server/next-auth/with-prisma.ts | 72 ++ .../template/extras/src/trpc/query-client.ts | 25 + .../template/extras/src/trpc/react.tsx | 76 ++ .../template/extras/src/trpc/server.ts | 30 + .../cli-old/template/extras/src/utils/api.ts | 68 ++ .../template/extras/start-database/mysql.sh | 54 ++ .../extras/start-database/postgres.sh | 55 ++ .../template/fm-addon/ProofKitAuth/de.xml | 518 ++++++++++ .../template/fm-addon/ProofKitAuth/en.xml | 518 ++++++++++ .../template/fm-addon/ProofKitAuth/es.xml | 518 ++++++++++ .../template/fm-addon/ProofKitAuth/fr.xml | 518 ++++++++++ .../template/fm-addon/ProofKitAuth/icon.png | Bin 0 -> 38399 bytes .../fm-addon/ProofKitAuth/icon@2x.png | Bin 0 -> 38399 bytes .../template/fm-addon/ProofKitAuth/info.json | 8 + .../fm-addon/ProofKitAuth/info_de.json | 11 + .../fm-addon/ProofKitAuth/info_en.json | 8 + .../fm-addon/ProofKitAuth/info_es.json | 11 + .../fm-addon/ProofKitAuth/info_fr.json | 11 + .../fm-addon/ProofKitAuth/info_it.json | 11 + .../fm-addon/ProofKitAuth/info_ja.json | 11 + .../fm-addon/ProofKitAuth/info_ko.json | 11 + .../fm-addon/ProofKitAuth/info_nl.json | 11 + .../fm-addon/ProofKitAuth/info_pt.json | 11 + .../fm-addon/ProofKitAuth/info_sv.json | 11 + .../fm-addon/ProofKitAuth/info_zh.json | 11 + .../template/fm-addon/ProofKitAuth/it.xml | 518 ++++++++++ .../template/fm-addon/ProofKitAuth/ja.xml | 518 ++++++++++ .../template/fm-addon/ProofKitAuth/ko.xml | 518 ++++++++++ .../template/fm-addon/ProofKitAuth/nl.xml | 518 ++++++++++ .../fm-addon/ProofKitAuth/preview.png | Bin 0 -> 38399 bytes .../template/fm-addon/ProofKitAuth/pt.xml | 518 ++++++++++ .../template/fm-addon/ProofKitAuth/sv.xml | 518 ++++++++++ .../fm-addon/ProofKitAuth/template.xml | Bin 0 -> 938018 bytes .../template/fm-addon/ProofKitAuth/zh.xml | 518 ++++++++++ .../template/fm-addon/ProofKitWV/de.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/en.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/es.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/fr.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/icon.png | Bin 0 -> 44003 bytes .../template/fm-addon/ProofKitWV/icon@2x.png | Bin 0 -> 44003 bytes .../template/fm-addon/ProofKitWV/info.json | 8 + .../template/fm-addon/ProofKitWV/info_de.json | 11 + .../template/fm-addon/ProofKitWV/info_en.json | 7 + .../template/fm-addon/ProofKitWV/info_es.json | 11 + .../template/fm-addon/ProofKitWV/info_fr.json | 11 + .../template/fm-addon/ProofKitWV/info_it.json | 11 + .../template/fm-addon/ProofKitWV/info_ja.json | 11 + .../template/fm-addon/ProofKitWV/info_ko.json | 11 + .../template/fm-addon/ProofKitWV/info_nl.json | 11 + .../template/fm-addon/ProofKitWV/info_pt.json | 11 + .../template/fm-addon/ProofKitWV/info_sv.json | 11 + .../template/fm-addon/ProofKitWV/info_zh.json | 11 + .../template/fm-addon/ProofKitWV/it.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/ja.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/ko.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/nl.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/preview.png | Bin 0 -> 44003 bytes .../template/fm-addon/ProofKitWV/pt.xml | 896 ++++++++++++++++++ .../fm-addon/ProofKitWV/records_de.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_en.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_es.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_fr.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_it.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_ja.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_ko.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_nl.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_pt.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_sv.xml | Bin 0 -> 1918 bytes .../fm-addon/ProofKitWV/records_zh.xml | Bin 0 -> 1918 bytes .../template/fm-addon/ProofKitWV/sv.xml | 896 ++++++++++++++++++ .../template/fm-addon/ProofKitWV/template.xml | Bin 0 -> 237896 bytes .../template/fm-addon/ProofKitWV/zh.xml | 896 ++++++++++++++++++ .../cli-old/template/nextjs-mantine/README.md | 27 + .../template/nextjs-mantine/_gitignore | 37 + .../template/nextjs-mantine/components.json | 21 + .../template/nextjs-mantine/next.config.ts | 12 + .../template/nextjs-mantine/package.json | 51 + .../nextjs-mantine/postcss.config.cjs | 15 + .../template/nextjs-mantine/proofkit.json | 7 + .../nextjs-mantine/public/favicon.ico | Bin 0 -> 15086 bytes .../nextjs-mantine/public/proofkit.png | Bin 0 -> 52140 bytes .../nextjs-mantine/src/app/(main)/layout.tsx | 6 + .../nextjs-mantine/src/app/(main)/page.tsx | 90 ++ .../nextjs-mantine/src/app/layout.tsx | 39 + .../nextjs-mantine/src/app/navigation.tsx | 12 + .../nextjs-mantine/src/components/AppLogo.tsx | 6 + .../components/AppShell/internal/AppShell.tsx | 21 + .../AppShell/internal/Header.module.css | 40 + .../components/AppShell/internal/Header.tsx | 34 + .../AppShell/internal/HeaderMobileMenu.tsx | 27 + .../AppShell/internal/HeaderNavLink.tsx | 35 + .../components/AppShell/internal/config.ts | 1 + .../AppShell/slot-header-center.tsx | 13 + .../components/AppShell/slot-header-left.tsx | 23 + .../AppShell/slot-header-mobile-content.tsx | 43 + .../components/AppShell/slot-header-right.tsx | 26 + .../template/nextjs-mantine/src/config/env.ts | 13 + .../src/config/theme/globals.css | 125 +++ .../src/config/theme/mantine-theme.ts | 22 + .../nextjs-mantine/src/server/safe-action.ts | 3 + .../src/utils/notification-helpers.ts | 32 + .../nextjs-mantine/src/utils/styles.ts | 6 + .../template/nextjs-mantine/tsconfig.json | 27 + .../template/nextjs-shadcn/.claude/CLAUDE.md | 327 +++++++ .../nextjs-shadcn/.cursor/rules/ultracite.mdc | 333 +++++++ .../nextjs-shadcn/.vscode/settings.json | 35 + .../cli-old/template/nextjs-shadcn/README.md | 27 + .../cli-old/template/nextjs-shadcn/_gitignore | 37 + .../cli-old/template/nextjs-shadcn/biome.json | 48 + .../template/nextjs-shadcn/components.json | 21 + .../template/nextjs-shadcn/next.config.ts | 8 + .../template/nextjs-shadcn/package.json | 38 + .../template/nextjs-shadcn/postcss.config.mjs | 5 + .../template/nextjs-shadcn/proofkit.json | 6 + .../template/nextjs-shadcn/public/favicon.ico | Bin 0 -> 15086 bytes .../nextjs-shadcn/public/proofkit.png | Bin 0 -> 52140 bytes .../nextjs-shadcn/src/app/(main)/layout.tsx | 6 + .../nextjs-shadcn/src/app/(main)/page.tsx | 124 +++ .../nextjs-shadcn/src/app/globals.css | 122 +++ .../template/nextjs-shadcn/src/app/layout.tsx | 35 + .../nextjs-shadcn/src/app/navigation.tsx | 12 + .../nextjs-shadcn/src/components/AppLogo.tsx | 6 + .../components/AppShell/internal/AppShell.tsx | 23 + .../AppShell/internal/Header.module.css | 33 + .../components/AppShell/internal/Header.tsx | 30 + .../AppShell/internal/HeaderMobileMenu.tsx | 25 + .../AppShell/internal/HeaderNavLink.tsx | 35 + .../components/AppShell/internal/config.ts | 1 + .../AppShell/slot-header-center.tsx | 13 + .../components/AppShell/slot-header-left.tsx | 23 + .../AppShell/slot-header-mobile-content.tsx | 43 + .../components/AppShell/slot-header-right.tsx | 25 + .../src/components/mode-toggle.tsx | 39 + .../src/components/providers.tsx | 13 + .../src/components/theme-provider.tsx | 11 + .../src/components/ui/button.tsx | 61 ++ .../src/components/ui/dropdown-menu.tsx | 267 ++++++ .../src/components/ui/sonner.tsx | 31 + .../template/nextjs-shadcn/src/lib/env.ts | 12 + .../template/nextjs-shadcn/src/lib/utils.ts | 6 + .../template/nextjs-shadcn/tsconfig.json | 41 + .../template/pages/nextjs/blank/page.tsx | 5 + .../pages/nextjs/table-edit/actions.ts | 24 + .../template/pages/nextjs/table-edit/page.tsx | 28 + .../pages/nextjs/table-edit/schema.ts | 4 + .../pages/nextjs/table-edit/table.tsx | 45 + .../nextjs/table-infinite-edit/actions.ts | 84 ++ .../pages/nextjs/table-infinite-edit/page.tsx | 23 + .../pages/nextjs/table-infinite-edit/query.ts | 87 ++ .../nextjs/table-infinite-edit/schema.ts | 4 + .../nextjs/table-infinite-edit/table.tsx | 130 +++ .../pages/nextjs/table-infinite/actions.ts | 62 ++ .../pages/nextjs/table-infinite/page.tsx | 11 + .../pages/nextjs/table-infinite/query.ts | 45 + .../pages/nextjs/table-infinite/table.tsx | 108 +++ .../template/pages/nextjs/table/page.tsx | 17 + .../template/pages/nextjs/table/table.tsx | 18 + .../template/pages/vite-wv/blank/index.tsx | 0 .../pages/vite-wv/table-edit/index.tsx | 72 ++ .../template/pages/vite-wv/table/index.tsx | 35 + .../template/vite-wv/.claude/launch.json | 18 + .../template/vite-wv/.vscode/settings.json | 11 + packages/cli-old/template/vite-wv/AGENTS.md | 1 + packages/cli-old/template/vite-wv/CLAUDE.md | 1 + packages/cli-old/template/vite-wv/_gitignore | 19 + .../cli-old/template/vite-wv/components.json | 21 + packages/cli-old/template/vite-wv/index.html | 13 + .../cli-old/template/vite-wv/package.json | 38 + .../vite-wv/proofkit-typegen.config.jsonc | 18 + .../cli-old/template/vite-wv/proofkit.json | 9 + .../template/vite-wv/scripts/filemaker.js | 96 ++ .../template/vite-wv/scripts/launch-fm.js | 19 + .../template/vite-wv/scripts/upload.js | 24 + packages/cli-old/template/vite-wv/src/App.tsx | 84 ++ .../cli-old/template/vite-wv/src/index.css | 96 ++ .../cli-old/template/vite-wv/src/lib/utils.ts | 6 + .../cli-old/template/vite-wv/src/main.tsx | 21 + .../cli-old/template/vite-wv/src/router.tsx | 57 ++ .../vite-wv/src/routes/query-demo.tsx | 37 + .../cli-old/template/vite-wv/tsconfig.json | 16 + .../cli-old/template/vite-wv/vite.config.ts | 18 + .../tests/browser-apps.smoke.test.ts} | 8 +- packages/cli-old/tests/cli.test.ts | 22 + .../init-non-interactive-failures.test.ts | 222 +++++ .../init-post-init-generation-errors.test.ts | 62 ++ .../tests/init-run-init-regression.test.ts | 197 ++++ .../tests/init-scaffold-contract.test.ts | 220 +++++ packages/cli-old/tests/setup.ts | 13 + packages/cli-old/tests/test-utils.ts | 70 ++ packages/cli-old/tests/webviewer-apps.test.ts | 155 +++ packages/cli-old/tsconfig.json | 14 + packages/cli-old/tsdown.config.ts | 54 ++ packages/cli-old/vitest.config.ts | 24 + packages/cli-old/vitest.smoke.config.ts | 18 + packages/cli/package.json | 23 +- packages/cli/src/cli/init.ts | 106 ++- packages/cli/src/consts.ts | 50 +- packages/{new => cli}/src/core/context.ts | 22 +- packages/{new => cli}/src/core/errors.ts | 0 .../{new => cli}/src/core/executeInitPlan.ts | 51 +- packages/{new => cli}/src/core/planInit.ts | 7 +- .../src/core/resolveInitRequest.ts | 12 + packages/{new => cli}/src/core/types.ts | 0 packages/cli/src/index.ts | 474 +++++++-- packages/{new => cli}/src/services/live.ts | 0 .../{new => cli}/src/utils/browserOpen.ts | 0 packages/{new => cli}/src/utils/http.ts | 0 .../{new => cli}/src/utils/packageManager.ts | 0 .../{new => cli}/src/utils/projectFiles.ts | 35 +- .../{new => cli}/src/utils/projectName.ts | 0 packages/{new => cli}/src/utils/prompts.ts | 0 packages/cli/src/utils/renderTitle.ts | 14 +- packages/{new => cli}/src/utils/versioning.ts | 0 packages/cli/tests/browser-apps.smoke.test.ts | 87 ++ packages/cli/tests/cli.test.ts | 117 ++- .../tests/default-command.test.ts | 6 +- packages/{new => cli}/tests/executor.test.ts | 0 packages/{new => cli}/tests/init-fixtures.ts | 6 + .../init-non-interactive-failures.test.ts | 222 +++++ .../init-post-init-generation-errors.test.ts | 62 ++ .../tests/init-run-init-regression.test.ts | 197 ++++ .../cli/tests/init-scaffold-contract.test.ts | 228 +++++ .../{new => cli}/tests/integration.test.ts | 68 +- packages/{new => cli}/tests/planner.test.ts | 1 + .../{new => cli}/tests/project-name.test.ts | 0 packages/{new => cli}/tests/prompts.test.ts | 0 .../{new => cli}/tests/resolve-init.test.ts | 0 packages/{new => cli}/tests/test-layer.ts | 0 packages/cli/tests/test-utils.ts | 12 +- packages/cli/tests/webviewer-apps.test.ts | 39 +- packages/cli/tsconfig.json | 8 +- packages/cli/tsdown.config.ts | 41 - packages/cli/vitest.config.ts | 12 +- .../vitest.smoke.config.ts} | 3 +- packages/create-proofkit/package.json | 6 +- packages/create-proofkit/src/index.js | 13 +- packages/create-proofkit/tests/index.test.js | 104 ++ packages/create-proofkit/vitest.config.ts | 8 + packages/new/package.json | 65 -- packages/new/src/utils/renderTitle.ts | 19 - packages/new/tests/cli.test.ts | 70 -- packages/new/tsconfig.json | 12 - packages/new/tsdown.config.ts | 14 - pnpm-lock.yaml | 564 ++++++----- 496 files changed, 40198 insertions(+), 659 deletions(-) create mode 100644 .changeset/proofkit-cli-major-migration.md create mode 100644 packages/cli-old/.yarnrc.yml create mode 100644 packages/cli-old/CHANGELOG.md create mode 100644 packages/cli-old/README.md create mode 100644 packages/cli-old/index.d.ts create mode 100644 packages/cli-old/package.json rename packages/{cli => cli-old}/proofkit-cli-1.1.8.tgz (100%) create mode 100644 packages/cli-old/src/cli/add/auth.ts create mode 100644 packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts create mode 100644 packages/cli-old/src/cli/add/data-source/filemaker.ts create mode 100644 packages/cli-old/src/cli/add/data-source/index.ts create mode 100644 packages/cli-old/src/cli/add/fmschema.ts create mode 100644 packages/cli-old/src/cli/add/index.ts create mode 100644 packages/cli-old/src/cli/add/page/index.ts create mode 100644 packages/cli-old/src/cli/add/page/post-install/table-infinite.ts create mode 100644 packages/cli-old/src/cli/add/page/post-install/table.ts create mode 100644 packages/cli-old/src/cli/add/page/templates.ts create mode 100644 packages/cli-old/src/cli/add/page/types.ts create mode 100644 packages/cli-old/src/cli/add/registry/getOptions.ts create mode 100644 packages/cli-old/src/cli/add/registry/http.ts create mode 100644 packages/cli-old/src/cli/add/registry/install.ts create mode 100644 packages/cli-old/src/cli/add/registry/listItems.ts create mode 100644 packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts create mode 100644 packages/cli-old/src/cli/add/registry/postInstall/index.ts create mode 100644 packages/cli-old/src/cli/add/registry/postInstall/package-script.ts create mode 100644 packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts create mode 100644 packages/cli-old/src/cli/add/registry/preflight.ts create mode 100644 packages/cli-old/src/cli/deploy/index.ts create mode 100644 packages/cli-old/src/cli/fmdapi.ts create mode 100644 packages/cli-old/src/cli/init.ts create mode 100644 packages/cli-old/src/cli/menu.ts create mode 100644 packages/cli-old/src/cli/ottofms.ts create mode 100644 packages/cli-old/src/cli/prompts.ts create mode 100644 packages/cli-old/src/cli/react-email.ts create mode 100644 packages/cli-old/src/cli/remove/data-source.ts create mode 100644 packages/cli-old/src/cli/remove/index.ts create mode 100644 packages/cli-old/src/cli/remove/page.ts create mode 100644 packages/cli-old/src/cli/remove/schema.ts create mode 100644 packages/cli-old/src/cli/tanstack-query.ts create mode 100644 packages/cli-old/src/cli/typegen/index.ts create mode 100644 packages/cli-old/src/cli/update/index.ts create mode 100644 packages/cli-old/src/cli/update/makeUpgradeCommand.ts create mode 100644 packages/cli-old/src/cli/utils.ts create mode 100644 packages/cli-old/src/consts.ts create mode 100644 packages/cli-old/src/generators/auth.ts create mode 100644 packages/cli-old/src/generators/fmdapi.ts create mode 100644 packages/cli-old/src/generators/route.ts create mode 100644 packages/cli-old/src/generators/tanstack-query.ts create mode 100644 packages/cli-old/src/globalOptions.ts create mode 100644 packages/cli-old/src/globals.d.ts create mode 100644 packages/cli-old/src/helpers/createProject.ts create mode 100644 packages/cli-old/src/helpers/fmHttp.ts create mode 100644 packages/cli-old/src/helpers/git.ts create mode 100644 packages/cli-old/src/helpers/installDependencies.ts create mode 100644 packages/cli-old/src/helpers/installPackages.ts create mode 100644 packages/cli-old/src/helpers/logNextSteps.ts create mode 100644 packages/cli-old/src/helpers/replaceText.ts create mode 100644 packages/cli-old/src/helpers/scaffoldProject.ts create mode 100644 packages/cli-old/src/helpers/selectBoilerplate.ts create mode 100644 packages/cli-old/src/helpers/setImportAlias.ts create mode 100644 packages/cli-old/src/helpers/shadcn-cli.ts create mode 100644 packages/cli-old/src/helpers/stealth-init.ts create mode 100644 packages/cli-old/src/helpers/version-fetcher.ts create mode 100644 packages/cli-old/src/index.ts create mode 100644 packages/cli-old/src/installers/auth-shared.ts create mode 100644 packages/cli-old/src/installers/better-auth.ts create mode 100644 packages/cli-old/src/installers/clerk.ts create mode 100644 packages/cli-old/src/installers/dependencyVersionMap.ts create mode 100644 packages/cli-old/src/installers/envVars.ts create mode 100644 packages/cli-old/src/installers/index.ts create mode 100644 packages/cli-old/src/installers/install-fm-addon.ts create mode 100644 packages/cli-old/src/installers/nextAuth.ts create mode 100644 packages/cli-old/src/installers/proofkit-auth.ts create mode 100644 packages/cli-old/src/installers/proofkit-webviewer.ts create mode 100644 packages/cli-old/src/installers/react-email.ts create mode 100644 packages/cli-old/src/state.ts create mode 100644 packages/cli-old/src/upgrades/cursorRules.ts create mode 100644 packages/cli-old/src/upgrades/index.ts create mode 100644 packages/cli-old/src/upgrades/shadcn.ts create mode 100644 packages/cli-old/src/utils/addPackageDependency.ts create mode 100644 packages/cli-old/src/utils/addToEnvs.ts create mode 100644 packages/cli-old/src/utils/formatting.ts create mode 100644 packages/cli-old/src/utils/getProofKitVersion.ts create mode 100644 packages/cli-old/src/utils/getUserPkgManager.ts create mode 100644 packages/cli-old/src/utils/isTTYError.ts create mode 100644 packages/cli-old/src/utils/logger.ts create mode 100644 packages/cli-old/src/utils/parseNameAndPath.ts create mode 100644 packages/cli-old/src/utils/parseSettings.ts create mode 100644 packages/cli-old/src/utils/proofkitReleaseChannel.ts create mode 100644 packages/cli-old/src/utils/removeTrailingSlash.ts create mode 100644 packages/cli-old/src/utils/renderTitle.ts create mode 100644 packages/cli-old/src/utils/renderVersionWarning.ts create mode 100644 packages/cli-old/src/utils/ts-morph.ts create mode 100644 packages/cli-old/src/utils/validateAppName.ts create mode 100644 packages/cli-old/src/utils/validateImportAlias.ts create mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc create mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc create mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc create mode 100644 packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc create mode 100644 packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc create mode 100644 packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc create mode 100644 packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc create mode 100644 packages/cli-old/template/extras/_cursor/rules/ui-components.mdc create mode 100644 packages/cli-old/template/extras/config/drizzle-config-mysql.ts create mode 100644 packages/cli-old/template/extras/config/drizzle-config-postgres.ts create mode 100644 packages/cli-old/template/extras/config/drizzle-config-sqlite.ts create mode 100644 packages/cli-old/template/extras/config/fmschema.config.mjs create mode 100644 packages/cli-old/template/extras/config/get-query-client.ts create mode 100644 packages/cli-old/template/extras/config/postcss.config.cjs create mode 100644 packages/cli-old/template/extras/config/query-provider-vite.tsx create mode 100644 packages/cli-old/template/extras/config/query-provider.tsx create mode 100644 packages/cli-old/template/extras/emailProviders/none/email.tsx create mode 100644 packages/cli-old/template/extras/emailProviders/plunk/email.tsx create mode 100644 packages/cli-old/template/extras/emailProviders/plunk/service.ts create mode 100644 packages/cli-old/template/extras/emailProviders/resend/email.tsx create mode 100644 packages/cli-old/template/extras/emailProviders/resend/service.ts create mode 100644 packages/cli-old/template/extras/emailTemplates/auth-code.tsx create mode 100644 packages/cli-old/template/extras/emailTemplates/generic.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx create mode 100644 packages/cli-old/template/extras/fmaddon-auth/middleware.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts create mode 100644 packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts create mode 100644 packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma create mode 100644 packages/cli-old/template/extras/prisma/schema/base.prisma create mode 100644 packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma create mode 100644 packages/cli-old/template/extras/prisma/schema/with-auth.prisma create mode 100644 packages/cli-old/template/extras/src/app/_components/post-tw.tsx create mode 100644 packages/cli-old/template/extras/src/app/_components/post.tsx create mode 100644 packages/cli-old/template/extras/src/app/api/auth/[...nextauth]/route.ts create mode 100644 packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts create mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx create mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx create mode 100644 packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx create mode 100644 packages/cli-old/template/extras/src/app/layout/base.tsx create mode 100644 packages/cli-old/template/extras/src/app/layout/main-shell.tsx create mode 100644 packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx create mode 100644 packages/cli-old/template/extras/src/app/layout/with-trpc.tsx create mode 100644 packages/cli-old/template/extras/src/app/layout/with-tw.tsx create mode 100644 packages/cli-old/template/extras/src/app/next-auth/layout.tsx create mode 100644 packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx create mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/action.ts create mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx create mode 100644 packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts create mode 100644 packages/cli-old/template/extras/src/app/page/base.tsx create mode 100644 packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx create mode 100644 packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx create mode 100644 packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx create mode 100644 packages/cli-old/template/extras/src/app/page/with-trpc.tsx create mode 100644 packages/cli-old/template/extras/src/app/page/with-tw.tsx create mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx create mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx create mode 100644 packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx create mode 100644 packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx create mode 100644 packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx create mode 100644 packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx create mode 100644 packages/cli-old/template/extras/src/env/with-auth.ts create mode 100644 packages/cli-old/template/extras/src/env/with-clerk.ts create mode 100644 packages/cli-old/template/extras/src/index.module.css create mode 100644 packages/cli-old/template/extras/src/middleware/clerk.ts create mode 100644 packages/cli-old/template/extras/src/middleware/next-auth.ts create mode 100644 packages/cli-old/template/extras/src/pages/_app/base.tsx create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-auth.tsx create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx create mode 100644 packages/cli-old/template/extras/src/pages/_app/with-tw.tsx create mode 100644 packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts create mode 100644 packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts create mode 100644 packages/cli-old/template/extras/src/pages/index/base.tsx create mode 100644 packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx create mode 100644 packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx create mode 100644 packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx create mode 100644 packages/cli-old/template/extras/src/pages/index/with-trpc.tsx create mode 100644 packages/cli-old/template/extras/src/pages/index/with-tw.tsx create mode 100644 packages/cli-old/template/extras/src/server/api/root.ts create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/base.ts create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts create mode 100644 packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/base.ts create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts create mode 100644 packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts create mode 100644 packages/cli-old/template/extras/src/server/data/users.ts create mode 100644 packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts create mode 100644 packages/cli-old/template/extras/src/server/db/db-prisma.ts create mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts create mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts create mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts create mode 100644 packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts create mode 100644 packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts create mode 100644 packages/cli-old/template/extras/src/server/next-auth/base.ts create mode 100644 packages/cli-old/template/extras/src/server/next-auth/password.ts create mode 100644 packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts create mode 100644 packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts create mode 100644 packages/cli-old/template/extras/src/trpc/query-client.ts create mode 100644 packages/cli-old/template/extras/src/trpc/react.tsx create mode 100644 packages/cli-old/template/extras/src/trpc/server.ts create mode 100644 packages/cli-old/template/extras/src/utils/api.ts create mode 100755 packages/cli-old/template/extras/start-database/mysql.sh create mode 100755 packages/cli-old/template/extras/start-database/postgres.sh create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/de.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/en.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/es.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/fr.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/icon.png create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/icon@2x.png create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_de.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_en.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_es.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_fr.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_it.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_ja.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_ko.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_nl.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_pt.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_sv.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/info_zh.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/it.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/ja.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/ko.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/nl.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/preview.png create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/pt.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/sv.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/template.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitAuth/zh.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/de.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/en.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/es.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/fr.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/icon.png create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/icon@2x.png create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_de.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_en.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_es.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_fr.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_it.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_ja.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_ko.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_nl.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_pt.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_sv.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/info_zh.json create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/it.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/ja.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/ko.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/nl.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/preview.png create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/pt.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_de.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_en.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_es.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_fr.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_it.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_ja.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_ko.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_nl.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_pt.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_sv.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/records_zh.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/sv.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/template.xml create mode 100644 packages/cli-old/template/fm-addon/ProofKitWV/zh.xml create mode 100644 packages/cli-old/template/nextjs-mantine/README.md create mode 100644 packages/cli-old/template/nextjs-mantine/_gitignore create mode 100644 packages/cli-old/template/nextjs-mantine/components.json create mode 100644 packages/cli-old/template/nextjs-mantine/next.config.ts create mode 100644 packages/cli-old/template/nextjs-mantine/package.json create mode 100644 packages/cli-old/template/nextjs-mantine/postcss.config.cjs create mode 100644 packages/cli-old/template/nextjs-mantine/proofkit.json create mode 100644 packages/cli-old/template/nextjs-mantine/public/favicon.ico create mode 100644 packages/cli-old/template/nextjs-mantine/public/proofkit.png create mode 100644 packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/app/layout.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx create mode 100644 packages/cli-old/template/nextjs-mantine/src/config/env.ts create mode 100644 packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css create mode 100644 packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts create mode 100644 packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts create mode 100644 packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts create mode 100644 packages/cli-old/template/nextjs-mantine/src/utils/styles.ts create mode 100644 packages/cli-old/template/nextjs-mantine/tsconfig.json create mode 100644 packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md create mode 100644 packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc create mode 100644 packages/cli-old/template/nextjs-shadcn/.vscode/settings.json create mode 100644 packages/cli-old/template/nextjs-shadcn/README.md create mode 100644 packages/cli-old/template/nextjs-shadcn/_gitignore create mode 100644 packages/cli-old/template/nextjs-shadcn/biome.json create mode 100644 packages/cli-old/template/nextjs-shadcn/components.json create mode 100644 packages/cli-old/template/nextjs-shadcn/next.config.ts create mode 100644 packages/cli-old/template/nextjs-shadcn/package.json create mode 100644 packages/cli-old/template/nextjs-shadcn/postcss.config.mjs create mode 100644 packages/cli-old/template/nextjs-shadcn/proofkit.json create mode 100644 packages/cli-old/template/nextjs-shadcn/public/favicon.ico create mode 100644 packages/cli-old/template/nextjs-shadcn/public/proofkit.png create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/globals.css create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/layout.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/app/navigation.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppLogo.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/config.ts create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/mode-toggle.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/providers.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/theme-provider.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/button.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/components/ui/sonner.tsx create mode 100644 packages/cli-old/template/nextjs-shadcn/src/lib/env.ts create mode 100644 packages/cli-old/template/nextjs-shadcn/src/lib/utils.ts create mode 100644 packages/cli-old/template/nextjs-shadcn/tsconfig.json create mode 100644 packages/cli-old/template/pages/nextjs/blank/page.tsx create mode 100644 packages/cli-old/template/pages/nextjs/table-edit/actions.ts create mode 100644 packages/cli-old/template/pages/nextjs/table-edit/page.tsx create mode 100644 packages/cli-old/template/pages/nextjs/table-edit/schema.ts create mode 100644 packages/cli-old/template/pages/nextjs/table-edit/table.tsx create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/actions.ts create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/page.tsx create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/query.ts create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/schema.ts create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite-edit/table.tsx create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/actions.ts create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/page.tsx create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/query.ts create mode 100644 packages/cli-old/template/pages/nextjs/table-infinite/table.tsx create mode 100644 packages/cli-old/template/pages/nextjs/table/page.tsx create mode 100644 packages/cli-old/template/pages/nextjs/table/table.tsx create mode 100644 packages/cli-old/template/pages/vite-wv/blank/index.tsx create mode 100644 packages/cli-old/template/pages/vite-wv/table-edit/index.tsx create mode 100644 packages/cli-old/template/pages/vite-wv/table/index.tsx create mode 100644 packages/cli-old/template/vite-wv/.claude/launch.json create mode 100644 packages/cli-old/template/vite-wv/.vscode/settings.json create mode 100644 packages/cli-old/template/vite-wv/AGENTS.md create mode 100644 packages/cli-old/template/vite-wv/CLAUDE.md create mode 100644 packages/cli-old/template/vite-wv/_gitignore create mode 100644 packages/cli-old/template/vite-wv/components.json create mode 100644 packages/cli-old/template/vite-wv/index.html create mode 100644 packages/cli-old/template/vite-wv/package.json create mode 100644 packages/cli-old/template/vite-wv/proofkit-typegen.config.jsonc create mode 100644 packages/cli-old/template/vite-wv/proofkit.json create mode 100644 packages/cli-old/template/vite-wv/scripts/filemaker.js create mode 100644 packages/cli-old/template/vite-wv/scripts/launch-fm.js create mode 100644 packages/cli-old/template/vite-wv/scripts/upload.js create mode 100644 packages/cli-old/template/vite-wv/src/App.tsx create mode 100644 packages/cli-old/template/vite-wv/src/index.css create mode 100644 packages/cli-old/template/vite-wv/src/lib/utils.ts create mode 100644 packages/cli-old/template/vite-wv/src/main.tsx create mode 100644 packages/cli-old/template/vite-wv/src/router.tsx create mode 100644 packages/cli-old/template/vite-wv/src/routes/query-demo.tsx create mode 100644 packages/cli-old/template/vite-wv/tsconfig.json create mode 100644 packages/cli-old/template/vite-wv/vite.config.ts rename packages/{cli/tests/browser-apps.test.ts => cli-old/tests/browser-apps.smoke.test.ts} (92%) create mode 100644 packages/cli-old/tests/cli.test.ts create mode 100644 packages/cli-old/tests/init-non-interactive-failures.test.ts create mode 100644 packages/cli-old/tests/init-post-init-generation-errors.test.ts create mode 100644 packages/cli-old/tests/init-run-init-regression.test.ts create mode 100644 packages/cli-old/tests/init-scaffold-contract.test.ts create mode 100644 packages/cli-old/tests/setup.ts create mode 100644 packages/cli-old/tests/test-utils.ts create mode 100644 packages/cli-old/tests/webviewer-apps.test.ts create mode 100644 packages/cli-old/tsconfig.json create mode 100644 packages/cli-old/tsdown.config.ts create mode 100644 packages/cli-old/vitest.config.ts create mode 100644 packages/cli-old/vitest.smoke.config.ts rename packages/{new => cli}/src/core/context.ts (94%) rename packages/{new => cli}/src/core/errors.ts (100%) rename packages/{new => cli}/src/core/executeInitPlan.ts (78%) rename packages/{new => cli}/src/core/planInit.ts (94%) rename packages/{new => cli}/src/core/resolveInitRequest.ts (95%) rename packages/{new => cli}/src/core/types.ts (100%) rename packages/{new => cli}/src/services/live.ts (100%) rename packages/{new => cli}/src/utils/browserOpen.ts (100%) rename packages/{new => cli}/src/utils/http.ts (100%) rename packages/{new => cli}/src/utils/packageManager.ts (100%) rename packages/{new => cli}/src/utils/projectFiles.ts (90%) rename packages/{new => cli}/src/utils/projectName.ts (100%) rename packages/{new => cli}/src/utils/prompts.ts (100%) rename packages/{new => cli}/src/utils/versioning.ts (100%) create mode 100644 packages/cli/tests/browser-apps.smoke.test.ts rename packages/{new => cli}/tests/default-command.test.ts (91%) rename packages/{new => cli}/tests/executor.test.ts (100%) rename packages/{new => cli}/tests/init-fixtures.ts (75%) create mode 100644 packages/cli/tests/init-non-interactive-failures.test.ts create mode 100644 packages/cli/tests/init-post-init-generation-errors.test.ts create mode 100644 packages/cli/tests/init-run-init-regression.test.ts create mode 100644 packages/cli/tests/init-scaffold-contract.test.ts rename packages/{new => cli}/tests/integration.test.ts (70%) rename packages/{new => cli}/tests/planner.test.ts (97%) rename packages/{new => cli}/tests/project-name.test.ts (100%) rename packages/{new => cli}/tests/prompts.test.ts (100%) rename packages/{new => cli}/tests/resolve-init.test.ts (100%) rename packages/{new => cli}/tests/test-layer.ts (100%) rename packages/{new/vitest.config.ts => cli/vitest.smoke.config.ts} (84%) create mode 100644 packages/create-proofkit/tests/index.test.js create mode 100644 packages/create-proofkit/vitest.config.ts delete mode 100644 packages/new/package.json delete mode 100644 packages/new/src/utils/renderTitle.ts delete mode 100644 packages/new/tests/cli.test.ts delete mode 100644 packages/new/tsconfig.json delete mode 100644 packages/new/tsdown.config.ts diff --git a/.changeset/proofkit-cli-major-migration.md b/.changeset/proofkit-cli-major-migration.md new file mode 100644 index 00000000..e872015f --- /dev/null +++ b/.changeset/proofkit-cli-major-migration.md @@ -0,0 +1,5 @@ +--- +"@proofkit/cli": major +--- + +Rewrite the CLI package for better observability, composability, and error tracing. diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index d9317656..f807bae6 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -62,7 +62,7 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Run Unit Tests + - name: Run Deterministic Contract Tests run: pnpm test build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6395033d..b57ee110 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,9 +59,12 @@ jobs: doppler configure set project proofkit doppler configure set config test - - name: Run Tests + - name: Run Deterministic Contract Tests run: pnpm test + - name: Run CLI External Integration Smoke Tests + run: doppler run -- pnpm --filter @proofkit/cli test:smoke + - name: Run fmodata E2E Tests run: pnpm --filter @proofkit/fmodata test:e2e diff --git a/packages/cli-old/.yarnrc.yml b/packages/cli-old/.yarnrc.yml new file mode 100644 index 00000000..c2e3ce63 --- /dev/null +++ b/packages/cli-old/.yarnrc.yml @@ -0,0 +1,5 @@ +packageExtensions: + chalk@5.0.1: + dependencies: + "#ansi-styles": npm:ansi-styles@6.1.0 + "#supports-color": npm:supports-color@9.2.2 diff --git a/packages/cli-old/CHANGELOG.md b/packages/cli-old/CHANGELOG.md new file mode 100644 index 00000000..7f74ccb5 --- /dev/null +++ b/packages/cli-old/CHANGELOG.md @@ -0,0 +1,285 @@ +# @proofgeist/kit + +## 2.0.0-beta.22 + +### Minor Changes + +- 5544f68: - cli: Revamp the WebViewer Vite template and harden `proofkit init` (ignore hidden files, improve non-interactive prompts, stop generating Cursor rules). + - cli: Install typegen skills locally when scaffolding projects. + - typegen: Add optional `fmHttp` config for using an FM HTTP proxy during metadata fetching. + - fmdapi/fmodata/webviewer: Add initial Codex skills for client and integration workflows. + +### Patch Changes + +- Updated dependencies [5544f68] +- Updated dependencies [f3980b1] +- Updated dependencies [8ca7a1e] +- Updated dependencies [1d4b69d] + - @proofkit/typegen@1.1.0-beta.17 + - @proofkit/fmdapi@5.1.0-beta.2 + +## 2.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [2df365d] + - @proofkit/typegen@1.1.0-beta.16 + +## 2.0.0-beta.20 + +### Patch Changes + +- @proofkit/typegen@1.1.0-beta.15 + +## 2.0.0-beta.19 + +### Patch Changes + +- Updated dependencies [4e048d1] + - @proofkit/typegen@1.1.0-beta.14 + +## 2.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [4928637] + - @proofkit/typegen@1.1.0-beta.13 + +## 2.0.0-beta.17 + +### Patch Changes + +- @proofkit/typegen@1.1.0-beta.12 + +## 2.0.0-beta.16 + +### Patch Changes + +- @proofkit/typegen@1.1.0-beta.11 + +## 2.0.0-beta.15 + +### Patch Changes + +- @proofkit/typegen@1.1.0-beta.10 + +## 2.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [eb7d751] + - @proofkit/typegen@1.1.0-beta.9 + +## 2.0.0-beta.13 + +### Patch Changes + +- @proofkit/typegen@1.1.0-beta.8 + +## 2.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [3b55d14] + - @proofkit/typegen@1.1.0-beta.7 + +## 2.0.0-beta.11 + +### Patch Changes + +- Updated dependencies + - @proofkit/typegen@1.1.0-beta.6 + +## 2.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [ae07372] +- Updated dependencies [23639ec] +- Updated dependencies [dfe52a7] + - @proofkit/typegen@1.1.0-beta.5 + +## 2.0.0-beta.9 + +### Patch Changes + +- 863e1e8: Update tooling to Biome +- Updated dependencies [7dbfd63] +- Updated dependencies [863e1e8] + - @proofkit/typegen@1.1.0-beta.4 + - @proofkit/fmdapi@5.0.3-beta.1 + +## 2.0.0-beta.8 + +### Patch Changes + +- @proofkit/typegen@1.1.0-beta.3 + +## 2.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [4d9d0e9] + - @proofkit/typegen@1.0.11-beta.1 + +## 1.1.8 + +### Patch Changes + +- 00177bf: Guard page add/remove against missing `src/app/navigation.tsx` so WebViewer apps don’t error when updating navigation. This safely no-ops when the navigation file isn’t present. +- Updated dependencies [7c602a9] +- Updated dependencies [a29ca94] + - @proofkit/typegen@1.0.10 + - @proofkit/fmdapi@5.0.2 + +## 1.1.5 + +### Patch Changes + +- Run typegen code directly instead of via execa +- error trap around formatting +- Remove shared-utils dep + +## 1.1.0 + +### Minor Changes + +- 7429a1e: Add simultaneous support for Shadcn. New projects will have Shadcn initialized automatically, and the upgrade command will offer to automatically add support for Shadcn to an existing ProofKit project. + +### Patch Changes + +- b483d67: Update formatting after typegen to be more consistent +- f0ddde2: Upgrade next-safe-action to v8 (and related dependencies) +- 7c87649: Fix getFieldNamesForSchema function + +## 1.0.0 + +### Major Changes + +- c348e37: Support @proofkit namespaced packages + +### Patch Changes + +- Updated dependencies [16fb8bd] +- Updated dependencies [16fb8bd] +- Updated dependencies [16fb8bd] + - @proofkit/fmdapi@5.0.0 + +## 0.3.2 + +### Patch Changes + +- 8986819: Fix: name argument in add command optional +- 47aad62: Make the auth installer spinner good + +## 0.3.1 + +### Patch Changes + +- 467d0f9: Add new menu command to expose all proofkit functions more easily +- 6da944a: Ensure using authedActionClient in existing actions after adding auth +- b211fbd: Deploy command: run build on Vercel instead of locally. Use flag --local-build to build locally like before +- 39648a9: Fix: Webviewer addon installation flow +- d0627b2: update base package versions + +## 0.3.0 + +### Minor Changes + +- 846ae9a: Add new upgrade command to upgrade ProofKit components in an existing project. To start, this command only adds/updates the cursor rules in your project. + +### Patch Changes + +- e07341a: Always use accessorFn for tables for better type errors + +## 0.2.3 + +### Patch Changes + +- 217eb5b: Fixed infinite table queries for other field names +- 217eb5b: New infinite table editable template + +## 0.2.2 + +### Patch Changes + +- ffae753: Better https parsing when prompting for the FileMaker Server URL +- 415be19: Add options for password strength in fm-addon auth. Default to not check for compromised passwords +- af5feba: Fix the launch-fm script for web viewer + +## 0.2.1 + +### Patch Changes + +- 6e44193: update helper text for npm after adding page +- 6e44193: additional supression of hydration warning +- 6e44193: move question about adding data source for new project +- 183988b: fix import path for reset password helper +- 6e44193: Make an initial commit when initializing git repo +- e0682aa: Copy cursor rules.mdc file into the base project. + +## 0.2.0 + +### Minor Changes + +- 6073cfe: Allow deploying a demo file to your server instead of having to pick an existing file + +### Patch Changes + +- d0f5c6e: Fix: post-install template functions not running + +## 0.1.2 + +### Patch Changes + +- 92cb423: fix: runtime error due to external shared package + +## 0.1.1 + +### Patch Changes + +- f88583c: prompt user to login to Vercel if needed during deploy command + +## 0.1.0 + +### Minor Changes + +- c019363: Add Deploy command for Vercel + +### Patch Changes + +- 0b7bf78: Allow setup without any data sources + +## 0.0.15 + +### Patch Changes + +- 1ff4aa7: Hide options for unsupported features in webviewer apps +- 5cfd0aa: Add infinite table page template +- 063859a: Added Template: Editable Table +- de0c2ab: update shebang in index +- b7ad0cf: Stream output from the typegen command + +## 0.0.6 + +### Patch Changes + +- Adding pages + +## 0.0.3 + +### Patch Changes + +- add typegen command for fm + +## 0.0.2 + +### Patch Changes + +- fix auth in init + +## 0.0.2-beta.0 + +### Patch Changes + +- fix auth in init diff --git a/packages/cli-old/README.md b/packages/cli-old/README.md new file mode 100644 index 00000000..86380fb3 --- /dev/null +++ b/packages/cli-old/README.md @@ -0,0 +1,19 @@ +

+ + 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.dev](https://proofkit.dev) diff --git a/packages/cli-old/index.d.ts b/packages/cli-old/index.d.ts new file mode 100644 index 00000000..61865039 --- /dev/null +++ b/packages/cli-old/index.d.ts @@ -0,0 +1,19 @@ +export interface RouteLink { + label: string; + type: "link"; + href: string; + icon?: React.ReactNode; + /** If true, the route will only be considered active if the path is exactly this value. */ + exactMatch?: boolean; +} + +export interface RouteFunction { + label: string; + type: "function"; + icon?: React.ReactNode; + onClick: () => void; + /** If true, the route will only be considered active if the path is exactly this value. */ + exactMatch?: boolean; +} + +export type ProofKitRoute = RouteLink | RouteFunction; diff --git a/packages/cli-old/package.json b/packages/cli-old/package.json new file mode 100644 index 00000000..dfcd3c24 --- /dev/null +++ b/packages/cli-old/package.json @@ -0,0 +1,128 @@ +{ + "name": "@proofkit/cli-old", + "version": "2.0.0-beta.22", + "private": true, + "description": "Create web application with the ProofKit stack", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/proofgeist/proofkit.git", + "directory": "packages/cli-old" + }, + "keywords": [ + "proofkit", + "filemaker", + "ottomatic", + "proofgeist", + "next.js", + "typescript" + ], + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "template", + "README.md", + "index.d.ts", + "LICENSE", + "CHANGELOG.md", + "package.json" + ], + "engines": { + "node": "^20.0.0 || ^22.0.0" + }, + "scripts": { + "typecheck": "tsc", + "build": "NODE_ENV=production tsdown && publint --strict", + "prepublishOnly": "pnpm build", + "dev": "tsdown --watch", + "clean": "rm -rf dist .turbo node_modules", + "start": "node dist/index.js", + "lint": "biome check . --write", + "lint:summary": "biome check . --reporter=summary", + "release": "changeset version", + "test": "pnpm test:contract", + "test:contract": "vitest run --config vitest.config.ts", + "test:smoke": "vitest run --config vitest.smoke.config.ts" + }, + "dependencies": { + "@better-fetch/fetch": "1.1.17", + "@clack/core": "^0.3.5", + "@clack/prompts": "^0.11.0", + "@inquirer/prompts": "^8.3.2", + "@proofkit/fmdapi": "workspace:*", + "@proofkit/typegen": "workspace:*", + "@types/glob": "^8.1.0", + "axios": "^1.13.2", + "chalk": "5.4.1", + "commander": "^14.0.2", + "dotenv": "^16.6.1", + "es-toolkit": "^1.43.0", + "execa": "^9.6.1", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.3", + "glob": "^11.1.0", + "gradient-string": "^2.0.2", + "handlebars": "^4.7.8", + "jiti": "^1.21.7", + "jsonc-parser": "^3.3.1", + "open": "^10.2.0", + "ora": "6.3.1", + "randomstring": "^1.3.1", + "semver": "^7.7.3", + "shadcn": "^2.10.0", + "sort-package-json": "^2.15.1", + "ts-morph": "^26.0.0" + }, + "devDependencies": { + "@auth/drizzle-adapter": "^1.11.1", + "@auth/prisma-adapter": "^1.6.0", + "@biomejs/biome": "2.3.11", + "@libsql/client": "^0.6.2", + "@planetscale/database": "^1.19.0", + "@prisma/adapter-planetscale": "^5.22.0", + "@prisma/client": "^5.22.0", + "@proofkit/registry": "workspace:*", + "@rollup/plugin-replace": "^6.0.3", + "@t3-oss/env-nextjs": "^0.10.1", + "@tanstack/react-query": "^5.90.16", + "@trpc/client": "11.0.0-rc.441", + "@trpc/next": "11.0.0-rc.441", + "@trpc/react-query": "11.0.0-rc.441", + "@trpc/server": "11.0.0-rc.441", + "@types/axios": "^0.14.4", + "@types/fs-extra": "^11.0.4", + "@types/gradient-string": "^1.1.6", + "@types/node": "^22.19.5", + "@types/randomstring": "^1.3.0", + "@types/react": "19.2.7", + "@types/semver": "^7.7.1", + "@vitest/coverage-v8": "^2.1.9", + "drizzle-kit": "^0.21.4", + "drizzle-orm": "^0.30.10", + "mysql2": "^3.16.0", + "next": "16.1.1", + "next-auth": "^4.24.13", + "postgres": "^3.4.8", + "prisma": "^5.22.0", + "publint": "^0.3.16", + "react": "19.2.3", + "react-dom": "19.2.3", + "superjson": "^2.2.6", + "tailwindcss": "^4.1.18", + "tsdown": "^0.14.2", + "type-fest": "^3.13.1", + "typescript": "^5.9.3", + "ultracite": "7.0.8", + "vitest": "^4.0.17", + "zod": "^4.3.5" + }, + "publishConfig": { + "access": "restricted" + } +} diff --git a/packages/cli/proofkit-cli-1.1.8.tgz b/packages/cli-old/proofkit-cli-1.1.8.tgz similarity index 100% rename from packages/cli/proofkit-cli-1.1.8.tgz rename to packages/cli-old/proofkit-cli-1.1.8.tgz diff --git a/packages/cli-old/src/cli/add/auth.ts b/packages/cli-old/src/cli/add/auth.ts new file mode 100644 index 00000000..cab277f9 --- /dev/null +++ b/packages/cli-old/src/cli/add/auth.ts @@ -0,0 +1,110 @@ +import chalk from "chalk"; +import { Command } from "commander"; +import { z } from "zod/v4"; +import { cancel, select } from "~/cli/prompts.js"; + +import { addAuth } from "~/generators/auth.js"; +import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; +import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; +import { getSettings } from "~/utils/parseSettings.js"; +import { abortIfCancel } from "../utils.js"; + +export async function runAddAuthAction() { + const settings = getSettings(); + if (settings.appType !== "browser") { + return cancel("Auth is not supported for your app type."); + } + if (settings.ui === "shadcn") { + return cancel("Adding auth is not yet supported for shadcn-based projects."); + } + + const authType = + state.authType ?? + abortIfCancel( + await select({ + message: "What auth provider do you want to use?", + options: [ + { + value: "fmaddon", + label: "FM Add-on Auth", + hint: "Self-hosted auth with email/password", + }, + { + value: "clerk", + label: "Clerk", + hint: "Hosted auth service with many providers", + }, + ], + }), + ); + + const type = z.enum(["clerk", "fmaddon"]).parse(authType); + state.authType = type; + + if (type === "fmaddon") { + const emailProviderAnswer = + state.emailProvider ?? + (isNonInteractiveMode() ? "none" : undefined) ?? + abortIfCancel( + await select({ + message: `What email provider do you want to use?\n${chalk.dim( + "Used to send email verification codes. If you skip this, the codes will be displayed here in your terminal.", + )}`, + options: [ + { + label: "Resend", + value: "resend", + hint: "Great dev experience", + }, + { + label: "Plunk", + value: "plunk", + hint: "Cheapest for <20k emails/mo, self-hostable", + }, + { label: "Other / I'll do it myself later", value: "none" }, + ], + }), + ); + + const emailProvider = z.enum(["plunk", "resend", "none"]).parse(emailProviderAnswer); + + state.emailProvider = emailProvider; + + await addAuth({ + options: { + type, + emailProvider: emailProvider === "none" ? undefined : emailProvider, + }, + }); + } else { + await addAuth({ options: { type } }); + } +} + +export const makeAddAuthCommand = () => { + const addAuthCommand = new Command("auth") + .description("Add authentication to your project") + .option("--authType ", "Type of auth provider to use") + .option("--emailProvider ", "Email provider to use (only for FM Add-on Auth)") + .option("--apiKey ", "API key to use for the email provider (only for FM Add-on Auth)") + .addOption(ciOption) + .addOption(nonInteractiveOption) + .addOption(debugOption) + + .action(async () => { + const settings = getSettings(); + if (settings.ui === "shadcn") { + throw new Error("Shadcn projects should add auth using the template registry"); + } + if (settings.auth.type !== "none") { + throw new Error("Auth already exists"); + } + await runAddAuthAction(); + }); + + addAuthCommand.hook("preAction", (thisCommand) => { + initProgramState(thisCommand.opts()); + }); + + return addAuthCommand; +}; diff --git a/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts b/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts new file mode 100644 index 00000000..61bcebe4 --- /dev/null +++ b/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts @@ -0,0 +1,96 @@ +import { createDataAPIKeyWithCredentials, getDeploymentStatus, startDeployment } from "~/cli/ottofms.js"; +import * as p from "~/cli/prompts.js"; + +export const filename = "ProofKitDemo.fmp12"; + +export async function deployDemoFile({ + url, + token, + operation, +}: { + url: URL; + token: string; + operation: "install" | "replace"; +}): Promise<{ apiKey: string }> { + const deploymentJSON = { + scheduled: false, + label: "Install ProofKit Demo", + deployments: [ + { + name: "Install ProofKit Demo", + source: { + type: "url", + url: "https://proofkit.dev/proofkit-demo/manifest.json", + }, + fileOperations: [ + { + target: { + fileName: filename, + }, + operation, + source: { + fileName: "ProofKitDemo.fmp12", + }, + location: { + folder: "default", + subFolder: "", + }, + }, + ], + concurrency: 1, + options: { + closeFilesAfterBuild: false, + keepFilesClosedAfterComplete: false, + transferContainerData: false, + }, + }, + ], + abortRemaining: false, + }; + + const spinner = p.spinner(); + spinner.start("Deploying ProofKit Demo file..."); + + const { + response: { subDeploymentIds }, + } = await startDeployment({ + payload: deploymentJSON, + url, + token, + }); + + const deploymentId = subDeploymentIds[0]; + if (!deploymentId) { + throw new Error("No deployment ID returned from the server"); + } + + while (true) { + // wait 2.5 seconds, then poll the status again + await new Promise((resolve) => setTimeout(resolve, 2500)); + + const { + response: { status, running }, + } = await getDeploymentStatus({ + url, + token, + deploymentId, + }); + if (!running) { + if (status !== "complete") { + throw new Error("Deployment didn't complete"); + } + break; + } + } + + const { apiKey } = await createDataAPIKeyWithCredentials({ + filename, + username: "admin", + password: "admin", + url, + }); + + spinner.stop(); + + return { apiKey }; +} diff --git a/packages/cli-old/src/cli/add/data-source/filemaker.ts b/packages/cli-old/src/cli/add/data-source/filemaker.ts new file mode 100644 index 00000000..1546f2a4 --- /dev/null +++ b/packages/cli-old/src/cli/add/data-source/filemaker.ts @@ -0,0 +1,441 @@ +import chalk from "chalk"; +import { SemVer } from "semver"; +import type { z } from "zod/v4"; +import { createDataAPIKey, getOttoFMSToken, listAPIKeys, listFiles } from "~/cli/ottofms.js"; +import * as p from "~/cli/prompts.js"; +import { abortIfCancel } from "~/cli/utils.js"; +import { addLayout, addToFmschemaConfig, ensureWebviewerFmHttpConfig } from "~/generators/fmdapi.js"; +import { getFmHttpStatus } from "~/helpers/fmHttp.js"; +import { fetchServerVersions } from "~/helpers/version-fetcher.js"; +import { isNonInteractiveMode } from "~/state.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { addToEnv } from "~/utils/addToEnvs.js"; +import { type dataSourceSchema, getSettings, setSettings } from "~/utils/parseSettings.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; +import { validateAppName } from "~/utils/validateAppName.js"; +import { runAddSchemaAction } from "../fmschema.js"; +import { deployDemoFile, filename } from "./deploy-demo-file.js"; + +export async function promptForFileMakerDataSource({ + projectDir, + ...opts +}: { + projectDir: string; + name?: string; + server?: string; + adminApiKey?: string; + fileName?: string; + dataApiKey?: string; + layoutName?: string; + schemaName?: string; +}) { + const settings = getSettings(); + + if (settings.appType === "webviewer") { + const fmHttpStatus = await getFmHttpStatus(); + const connectedFileName = fmHttpStatus.connectedFiles[0]; + const localDataSourceName = opts.name ?? "filemaker"; + + if (!opts.server && fmHttpStatus.healthy && connectedFileName) { + addPackageDependency({ + projectDir, + dependencies: ["@proofkit/fmdapi"], + devMode: false, + }); + + await ensureWebviewerFmHttpConfig({ + projectDir, + connectedFileName, + dataSourceName: localDataSourceName, + baseUrl: fmHttpStatus.baseUrl, + }); + + // Persist the datasource in project settings + const newDataSource: z.infer = { + type: "fm", + name: localDataSourceName, + envNames: + localDataSourceName === "filemaker" + ? { + database: "FM_DATABASE", + server: "FM_SERVER", + apiKey: "OTTO_API_KEY", + } + : { + database: `${localDataSourceName.toUpperCase()}_FM_DATABASE`, + server: `${localDataSourceName.toUpperCase()}_FM_SERVER`, + apiKey: `${localDataSourceName.toUpperCase()}_OTTO_API_KEY`, + }, + }; + settings.dataSources.push(newDataSource); + setSettings(settings); + + if (opts.layoutName && opts.schemaName) { + await addLayout({ + projectDir, + dataSourceName: localDataSourceName, + schemas: [ + { + layoutName: opts.layoutName, + schemaName: opts.schemaName, + valueLists: "allowEmpty", + }, + ], + }); + } else if (opts.layoutName || opts.schemaName) { + throw new Error("Both --layoutName and --schemaName must be provided together."); + } else { + p.note( + `Detected local FM HTTP at ${fmHttpStatus.baseUrl} with connected file "${connectedFileName}". Edit ${chalk.cyan( + "proofkit-typegen.config.jsonc", + )} to add layouts, then run ${chalk.cyan("pnpm typegen")} or ${chalk.cyan("pnpm typegen:ui")}.`, + "Local FileMaker detected", + ); + } + + return; + } + + if (!opts.server && isNonInteractiveMode()) { + throw new Error( + "No local FM HTTP connection was detected and no FileMaker server was provided. Start the local FM HTTP proxy with a connected file or rerun with --server.", + ); + } + + if (!opts.server) { + const fallbackAction = abortIfCancel( + await p.select({ + message: + "Local FM HTTP was not detected. Do you want to continue with hosted FileMaker server setup or skip for now?", + options: [ + { + label: "Continue with hosted setup", + value: "hosted", + }, + { + label: "Skip for now", + value: "skip", + }, + ], + }), + ); + + if (fallbackAction === "skip") { + p.note( + `You can come back later with ${chalk.cyan("proofkit add data")} after starting FM HTTP locally or when you have a hosted server ready.`, + ); + return; + } + } + } + + const existingFmDataSourceNames = settings.dataSources.filter((ds) => ds.type === "fm").map((ds) => ds.name); + + const server = await getValidFileMakerServerUrl(opts.server); + + const canDoBrowserLogin = server.ottoVersion && server.ottoVersion.compare(new SemVer("4.7.0")) > 0; + + if (!(canDoBrowserLogin || opts.adminApiKey)) { + return p.cancel( + "OttoFMS 4.7.0 or later is required to auto-login with this CLI. Please install/upgrade OttoFMS on your server, or pass an Admin API key with the --adminApiKey flag then try again", + ); + } + + const token = opts.adminApiKey || (await getOttoFMSToken({ url: server.url })).token; + + const fileList = await listFiles({ url: server.url, token }); + const demoFileExists = fileList.map((f) => f.filename.replace(".fmp12", "")).includes(filename.replace(".fmp12", "")); + let fmFile = opts.fileName; + while (true) { + fmFile = + opts.fileName || + abortIfCancel( + await p.searchSelect({ + message: `Which file would you like to connect to? ${chalk.dim("(TIP: Select the file where your data is stored)")}`, + searchLabel: "Search files", + emptyMessage: "No matching files found.", + options: [ + { + value: "$deployDemoFile", + label: "Deploy NEW ProofKit Demo File", + hint: "Use OttoFMS to deploy a new file for testing", + keywords: ["demo", "proofkit"], + }, + ...fileList + .sort((a, b) => a.filename.localeCompare(b.filename)) + .map((file) => ({ + value: file.filename, + label: file.filename, + hint: file.status, + keywords: [file.filename], + })), + ], + }), + ); + + if (fmFile !== "$deployDemoFile") { + break; + } + + if (demoFileExists) { + const replace = abortIfCancel( + await p.confirm({ + message: "The demo file already exists, do you want to replace it with a fresh copy?", + active: "Yes, replace", + inactive: "No, select another file", + initialValue: false, + }), + ); + if (replace) { + break; + } + } else { + break; + } + } + + if (!fmFile) { + throw new Error("No file selected"); + } + + let dataApiKey = opts.dataApiKey; + if (fmFile === "$deployDemoFile") { + const { apiKey } = await deployDemoFile({ + url: server.url, + token, + operation: demoFileExists ? "replace" : "install", + }); + dataApiKey = apiKey; + fmFile = filename; + opts.layoutName = opts.layoutName ?? "API_Contacts"; + opts.schemaName = opts.schemaName ?? "Contacts"; + } else { + const allApiKeys = await listAPIKeys({ url: server.url, token }); + const thisFileApiKeys = allApiKeys.filter((key) => key.database === fmFile); + + if (!dataApiKey && thisFileApiKeys.length > 0) { + const selectedKey = abortIfCancel( + await p.searchSelect({ + message: `Which OttoFMS Data API key would you like to use? ${chalk.dim(`(This determines the access that you'll have to the data in this file)`)}`, + searchLabel: "Search API keys", + emptyMessage: "No matching API keys found.", + options: [ + ...thisFileApiKeys.map((key) => ({ + value: key.key, + label: `${chalk.bold(key.label)} - ${key.user}`, + hint: `${key.key.slice(0, 5)}...${key.key.slice(-4)}`, + keywords: [key.label, key.user, key.database], + })), + { + value: "create", + label: "Create a new API key", + hint: "Requires FileMaker credentials for this file", + keywords: ["create", "new"], + }, + ], + }), + ); + if (typeof selectedKey !== "string") { + throw new Error("Invalid key"); + } + if (selectedKey !== "create") { + dataApiKey = selectedKey; + } + } + + if (!dataApiKey) { + // data api was not provided, prompt to create a new one + const resp = await createDataAPIKey({ + filename: fmFile, + url: server.url, + }); + dataApiKey = resp.apiKey; + } + } + if (!dataApiKey) { + throw new Error("No API key"); + } + + const name = + existingFmDataSourceNames.length === 0 + ? "filemaker" + : (opts.name ?? + abortIfCancel( + await p.text({ + message: "What do you want to call this data source?", + validate: (value) => { + if (value === "filemaker") { + return "That name is reserved"; + } + + // require name to be unique + if (existingFmDataSourceNames?.includes(value)) { + return "That name is already in use in this project, pick something unique"; + } + + // require name to be alphanumeric, lowercase, etc + return validateAppName(value); + }, + }), + )); + + const newDataSource: z.infer = { + type: "fm", + name, + envNames: + name === "filemaker" + ? { + database: "FM_DATABASE", + server: "FM_SERVER", + apiKey: "OTTO_API_KEY", + } + : { + database: `${name.toUpperCase()}_FM_DATABASE`, + server: `${name.toUpperCase()}_FM_SERVER`, + apiKey: `${name.toUpperCase()}_OTTO_API_KEY`, + }, + }; + + const project = getNewProject(projectDir); + + const schemaFile = await addToEnv({ + projectDir, + project, + envs: [ + { + name: newDataSource.envNames.database, + zodValue: `z.string().endsWith(".fmp12")`, + defaultValue: fmFile, + type: "server", + }, + { + name: newDataSource.envNames.server, + zodValue: "z.string().url()", + type: "server", + defaultValue: server.url.origin, + }, + { + name: newDataSource.envNames.apiKey, + zodValue: `z.string().startsWith("dk_") as z.ZodType`, + type: "server", + defaultValue: dataApiKey, + }, + ], + }); + + const fmdapiImport = schemaFile.getImportDeclaration((imp) => imp.getModuleSpecifierValue() === "@proofkit/fmdapi"); + if (fmdapiImport) { + fmdapiImport + .getNamedImports() + .find((imp) => imp.getName() === "OttoAPIKey") + ?.remove(); + fmdapiImport.addNamedImport({ name: "OttoAPIKey", isTypeOnly: true }); + } else { + schemaFile.addImportDeclaration({ + namedImports: [{ name: "OttoAPIKey", isTypeOnly: true }], + moduleSpecifier: "@proofkit/fmdapi", + }); + } + + addPackageDependency({ + projectDir, + dependencies: ["@proofkit/fmdapi"], + devMode: false, + }); + + settings.dataSources.push(newDataSource); + setSettings(settings); + + addToFmschemaConfig({ + dataSourceName: name, + envNames: name === "filemaker" ? undefined : newDataSource.envNames, + }); + + await formatAndSaveSourceFiles(project); + + // now prompt for layout + await runAddSchemaAction({ + settings, + sourceName: name, + projectDir, + layoutName: opts.layoutName, + schemaName: opts.schemaName, + valueLists: "allowEmpty", + }); +} + +async function getValidFileMakerServerUrl(defaultServerUrl?: string | undefined): Promise<{ + url: URL; + fmsVersion: SemVer; + ottoVersion: SemVer | null; +}> { + const spinner = p.spinner(); + let url: URL | null = null; + let fmsVersion: SemVer | null = null; + let ottoVersion: SemVer | null = null; + let serverUrlToUse = defaultServerUrl; + + while (fmsVersion === null) { + const serverUrl = + serverUrlToUse ?? + abortIfCancel( + await p.text({ + message: `What is the URL of your FileMaker Server?\n${chalk.cyan("TIP: You can copy any valid path on the server and paste it here.")}`, + validate: (value) => { + try { + // try to make sure the url is https + let normalizedValue = value; + if (!normalizedValue.startsWith("https://")) { + if (normalizedValue.startsWith("http://")) { + normalizedValue = normalizedValue.replace("http://", "https://"); + } else { + normalizedValue = `https://${normalizedValue}`; + } + } + + // try to make sure the url is valid + new URL(normalizedValue); + return; + } catch { + return "Please enter a valid URL"; + } + }, + }), + ); + + try { + url = new URL(serverUrl); + } catch { + p.log.error(`Invalid URL: ${serverUrl.toString()}`); + continue; + } + + spinner.start("Validating Server URL..."); + + // check for FileMaker and Otto versions + const { fmsInfo, ottoInfo } = await fetchServerVersions({ + url: url.origin, + }); + + spinner.stop(); + + const fmsVersionString = fmsInfo.ServerVersion.split(" ")[0]; + if (!fmsVersionString) { + p.log.error("Unable to parse FileMaker Server version"); + serverUrlToUse = undefined; + continue; + } + fmsVersion = new SemVer(fmsVersionString); + ottoVersion = ottoInfo?.Otto.version ? new SemVer(ottoInfo.Otto.version) : null; + serverUrlToUse = undefined; + } + + if (url === null) { + throw new Error("Unable to get FileMaker Server URL"); + } + + p.note(`🎉 FileMaker Server version ${fmsVersion} detected \n + ${ottoVersion ? `🎉 OttoFMS version ${ottoVersion} detected` : "❌ OttoFMS not detected"}`); + + return { url, ottoVersion, fmsVersion }; +} diff --git a/packages/cli-old/src/cli/add/data-source/index.ts b/packages/cli-old/src/cli/add/data-source/index.ts new file mode 100644 index 00000000..6d73789b --- /dev/null +++ b/packages/cli-old/src/cli/add/data-source/index.ts @@ -0,0 +1,46 @@ +import { Command } from "commander"; +import { z } from "zod/v4"; +import * as p from "~/cli/prompts.js"; + +import { ensureProofKitProject } from "~/cli/utils.js"; +import { ciOption, nonInteractiveOption } from "~/globalOptions.js"; +import { initProgramState } from "~/state.js"; +import { promptForFileMakerDataSource } from "./filemaker.js"; + +const dataSourceType = z.enum(["fm", "supabase"]); +export const runAddDataSourceCommand = async () => { + const dataSource = dataSourceType.parse( + await p.select({ + message: "Which data souce do you want to add?", + options: [ + { label: "FileMaker", value: "fm" }, + { label: "Supabase", value: "supabase" }, + ], + }), + ); + + if (dataSource === "supabase") { + throw new Error("Not implemented"); + } + if (dataSource === "fm") { + await promptForFileMakerDataSource({ projectDir: process.cwd() }); + } else { + throw new Error("Invalid data source"); + } +}; + +export const makeAddDataSourceCommand = () => { + const addDataSourceCommand = new Command("data"); + addDataSourceCommand.description("Add a new data source to your project"); + addDataSourceCommand.addOption(ciOption); + addDataSourceCommand.addOption(nonInteractiveOption); + + addDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => { + initProgramState(actionCommand.opts()); + const settings = ensureProofKitProject({ commandName: "add" }); + actionCommand.setOptionValue("settings", settings); + }); + + // addDataSourceCommand.action(); + return addDataSourceCommand; +}; diff --git a/packages/cli-old/src/cli/add/fmschema.ts b/packages/cli-old/src/cli/add/fmschema.ts new file mode 100644 index 00000000..b63dc427 --- /dev/null +++ b/packages/cli-old/src/cli/add/fmschema.ts @@ -0,0 +1,216 @@ +import path from "node:path"; +import type { OttoAPIKey } from "@proofkit/fmdapi"; +import type { ValueListsOptions } from "@proofkit/typegen/config"; +import chalk from "chalk"; +import { Command } from "commander"; +import dotenv from "dotenv"; +import { z } from "zod/v4"; +import * as p from "~/cli/prompts.js"; +import { addLayout, getExistingSchemas } from "~/generators/fmdapi.js"; +import { state } from "~/state.js"; +import { getSettings, type Settings } from "~/utils/parseSettings.js"; +import { commonFileMakerLayoutPrefixes, getLayouts } from "../fmdapi.js"; +import { abortIfCancel } from "../utils.js"; + +// Regex to validate JavaScript variable names +const VALID_JS_VARIABLE_NAME = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; + +export const runAddSchemaAction = async (opts?: { + projectDir?: string; + settings: Settings; + sourceName?: string; + layoutName?: string; + schemaName?: string; + valueLists?: ValueListsOptions; +}) => { + const settings = getSettings(); + const projectDir = state.projectDir; + let sourceName = opts?.sourceName; + if (sourceName) { + sourceName = opts?.sourceName; + } else if (settings.dataSources.filter((s) => s.type === "fm").length > 1) { + // if there is more than one fm data source, we need to prompt for which one to add the layout to + const dataSourceName = await p.select({ + message: "Which FileMaker data source do you want to add a layout to?", + options: settings.dataSources.filter((s) => s.type === "fm").map((s) => ({ label: s.name, value: s.name })), + }); + if (p.isCancel(dataSourceName)) { + p.cancel(); + process.exit(0); + } + sourceName = z.string().parse(dataSourceName); + } + + if (!sourceName) { + sourceName = "filemaker"; + } + + const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === sourceName); + if (!dataSource) { + throw new Error(`FileMaker data source ${sourceName} not found in your ProofKit config`); + } + + const spinner = p.spinner(); + spinner.start("Loading layouts from your FileMaker file..."); + if (settings.envFile) { + dotenv.config({ + path: path.join(projectDir, settings.envFile), + }); + } + + const dataApiKey = process.env[dataSource.envNames.apiKey]; + const fmFile = process.env[dataSource.envNames.database]; + const server = process.env[dataSource.envNames.server]; + + if (!(dataApiKey && fmFile && server)) { + spinner.stop("Failed to load layouts"); + p.cancel("Missing required environment variables. Please check your .env file."); + process.exit(1); + } + + // Validate API key format + if (!(dataApiKey.startsWith("KEY_") || dataApiKey.startsWith("dk_"))) { + spinner.stop("Failed to load layouts"); + p.cancel("Invalid API key format. API key must start with 'KEY_' or 'dk_'."); + process.exit(1); + } + + // Type assertion after validation + const validatedApiKey: OttoAPIKey = dataApiKey as OttoAPIKey; + + const layouts = await getLayouts({ + dataApiKey: validatedApiKey, + fmFile, + server, + }); + + const existingConfigResults = getExistingSchemas({ + projectDir, + dataSourceName: sourceName, + }); + + const existingLayouts = existingConfigResults.map((s) => s.layout).filter(Boolean); + + const existingSchemas = existingConfigResults.map((s) => s.schemaName).filter(Boolean); + + spinner.stop("Loaded layouts from your FileMaker file"); + + if (existingLayouts.length > 0) { + p.note(existingLayouts.join("\n"), "Detected existing layouts in your project"); + } + + // list other common layout names to exclude + existingLayouts.push("-"); + + let passedInLayoutName: string | undefined = opts?.layoutName; + if (passedInLayoutName === "" || !layouts.includes(passedInLayoutName ?? "")) { + passedInLayoutName = undefined; + } + + const selectedLayout = + passedInLayoutName ?? + abortIfCancel( + await p.searchSelect({ + message: "Select a new layout to read data from", + searchLabel: "Search layouts", + emptyMessage: "No matching layouts found.", + options: layouts + .filter((layout) => !existingLayouts.includes(layout)) + .map((layout) => ({ + label: layout, + value: layout, + keywords: [layout], + })), + }), + ); + + const defaultSchemaName = getDefaultSchemaName(selectedLayout); + const schemaName = + opts?.schemaName || + abortIfCancel( + await p.text({ + message: `Enter a friendly name for the new schema.\n${chalk.dim("This will the name by which you refer to this layout in your codebase")}`, + // initialValue: selectedLayout, + defaultValue: defaultSchemaName, + placeholder: defaultSchemaName, + validate: (input) => { + if (input === "") { + return; // allow empty input for the default value + } + // ensure the input is a valid JS variable name + if (!VALID_JS_VARIABLE_NAME.test(input)) { + return "Name must consist of only alphanumeric characters, '_', and must not start with a number"; + } + if (existingSchemas.includes(input)) { + return "Schema name must be unique"; + } + return; + }, + }), + ).toString(); + + const valueLists = + opts?.valueLists ?? + ((await p.select({ + message: `Should we use value lists on this layout?\n${chalk.dim( + "This will allow fields that contain a value list to be auto-completed in typescript and also validated to prevent incorrect values", + )}`, + options: [ + { + label: "Yes, but allow empty fields", + value: "allowEmpty", + hint: "Empty fields or values that don't match the value list will be converted to an empty string", + }, + { + label: "Yes; empty values should fail validation", + value: "strict", + hint: "Empty fields or values that don't match the value list will cause validation to fail", + }, + { + label: "No, ignore value lists", + value: "ignore", + hint: "Fields will just be typed as strings", + }, + ], + })) as ValueListsOptions); + + const valueListsValidated = z.enum(["ignore", "allowEmpty", "strict"]).catch("ignore").parse(valueLists); + + await addLayout({ + runCodegen: true, + projectDir, + dataSourceName: sourceName, + schemas: [ + { + layoutName: selectedLayout, + schemaName, + valueLists: valueListsValidated, + }, + ], + }); + + p.outro(`Layout "${selectedLayout}" added to your project as "${schemaName}"`); +}; + +export const makeAddSchemaCommand = () => { + const addSchemaCommand = new Command("layout") + .alias("schema") + .description("Add a new layout to your fmschema file") + .action(async (opts: { settings: Settings }) => { + const settings = opts.settings; + + await runAddSchemaAction({ settings }); + }); + + return addSchemaCommand; +}; + +function getDefaultSchemaName(layout: string) { + let schemaName = layout.replace(/[-\s]/g, "_"); + for (const prefix of commonFileMakerLayoutPrefixes) { + if (schemaName.startsWith(prefix)) { + schemaName = schemaName.replace(prefix, ""); + } + } + return schemaName; +} diff --git a/packages/cli-old/src/cli/add/index.ts b/packages/cli-old/src/cli/add/index.ts new file mode 100644 index 00000000..b3e0d543 --- /dev/null +++ b/packages/cli-old/src/cli/add/index.ts @@ -0,0 +1,190 @@ +import type { RegistryIndex } from "@proofkit/registry"; +import { Command } from "commander"; +import { capitalize, groupBy, uniq } from "es-toolkit"; +import ora from "ora"; +import { select } from "~/cli/prompts.js"; +import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; +import { initProgramState, state } from "~/state.js"; +import { logger } from "~/utils/logger.js"; +import { getSettings, type Settings } from "~/utils/parseSettings.js"; +import { runAddReactEmailCommand } from "../react-email.js"; +import { runAddTanstackQueryCommand } from "../tanstack-query.js"; +import { abortIfCancel, ensureProofKitProject } from "../utils.js"; +import { makeAddAuthCommand, runAddAuthAction } from "./auth.js"; +import { makeAddDataSourceCommand, runAddDataSourceCommand } from "./data-source/index.js"; +import { makeAddSchemaCommand, runAddSchemaAction } from "./fmschema.js"; +import { makeAddPageCommand, runAddPageAction } from "./page/index.js"; +import { installFromRegistry } from "./registry/install.js"; +import { listItems } from "./registry/listItems.js"; +import { preflightAddCommand } from "./registry/preflight.js"; + +const runAddFromRegistry = async (_options?: { noInstall?: boolean }) => { + const settings = getSettings(); + + const spinner = ora("Loading available components...").start(); + let items: RegistryIndex; + try { + items = await listItems(); + } catch (error) { + spinner.fail("Failed to load registry components"); + logger.error(error); + return; + } + + const itemsNotInstalled = items.filter((item) => !settings.registryTemplates.includes(item.name)); + + const groupedByCategory = groupBy(itemsNotInstalled, (item) => item.category); + const categories = uniq(itemsNotInstalled.map((item) => item.category)); + + spinner.succeed(); + + const addType = abortIfCancel( + await select({ + message: "What do you want to add to your project?", + options: [ + // if there are pages available to install, show them first + ...(categories.includes("page") ? [{ label: "Page", value: "page" }] : []), + + // only show schema option if there is at least one data source + ...(settings.dataSources.length > 0 + ? [ + { + label: "Schema", + value: "schema", + hint: "load data from a new table or layout from an existing data source", + }, + ] + : []), + + { + label: "Data Source", + value: "data", + hint: "to connect to a new database or FileMaker file", + }, + + // show the rest of the categories + ...categories + .filter((category) => category !== "page") + .map((category) => ({ + label: capitalize(category), + value: category, + })), + ], + }), + ); + + if (addType === "schema") { + await runAddSchemaAction(); + } else if (addType === "data") { + await runAddDataSourceCommand(); + } else if ((categories as string[]).includes(addType)) { + // one of the categories + const itemsFromCategory = groupedByCategory[addType as keyof typeof groupedByCategory]; + + const itemName = abortIfCancel( + await select({ + message: `Select a ${addType} to add to your project`, + options: itemsFromCategory.map((item) => ({ + label: item.title, + hint: item.description, + value: item.name, + })), + }), + ); + + await installFromRegistry(itemName); + } else { + logger.error(`Could not find any available components in the category "${addType}"`); + } +}; + +export const runAdd = async (name: string | undefined, options?: { noInstall?: boolean }) => { + if (name === "tanstack-query") { + return await runAddTanstackQueryCommand(); + } + if (name !== undefined) { + // an arbitrary name was provided, so we'll try to install from the registry + return await installFromRegistry(name); + } + + let settings: Settings; + try { + settings = getSettings(); + } catch { + await preflightAddCommand(); + return await runAddFromRegistry(options); + } + + if (settings.ui === "shadcn") { + return await runAddFromRegistry(options); + } + ensureProofKitProject({ commandName: "add" }); + + const addType = abortIfCancel( + await select({ + message: "What do you want to add to your project?", + options: [ + { label: "Page", value: "page" }, + // only show schema option if there is at least one data source + ...(settings.dataSources.length > 0 + ? [ + { + label: "Schema", + value: "schema", + hint: "load data from a new table or layout from an existing data source", + }, + ] + : []), + { label: "React Email", value: "react-email" }, + { + label: "Data Source", + value: "data", + hint: "to connect to a new database or FileMaker file", + }, + ...(settings.auth.type === "none" && settings.appType === "browser" ? [{ label: "Auth", value: "auth" }] : []), + ], + }), + ); + + if (addType === "auth") { + await runAddAuthAction(); + } else if (addType === "data") { + await runAddDataSourceCommand(); + } else if (addType === "page") { + await runAddPageAction(); + } else if (addType === "schema") { + await runAddSchemaAction(); + } else if (addType === "react-email") { + await runAddReactEmailCommand({ noInstall: options?.noInstall }); + } +}; + +export const makeAddCommand = () => { + const addCommand = new Command("add") + .description("Add a new component to your project") + .argument("[name]", "Type of component to add") + .addOption(ciOption) + .addOption(nonInteractiveOption) + .addOption(debugOption) + .option("--noInstall", "Do not run your package manager install command", false) + .action(async (name, options) => { + await runAdd(name, options); + }); + + addCommand.hook("preAction", (_thisCommand, _actionCommand) => { + // console.log("preAction", _actionCommand.opts()); + initProgramState(_actionCommand.opts()); + state.baseCommand = "add"; + }); + addCommand.hook("preSubcommand", (_thisCommand, _subCommand) => { + // console.log("preSubcommand", _subCommand.opts()); + initProgramState(_subCommand.opts()); + state.baseCommand = "add"; + }); + + addCommand.addCommand(makeAddAuthCommand()); + addCommand.addCommand(makeAddPageCommand()); + addCommand.addCommand(makeAddSchemaCommand()); + addCommand.addCommand(makeAddDataSourceCommand()); + return addCommand; +}; diff --git a/packages/cli-old/src/cli/add/page/index.ts b/packages/cli-old/src/cli/add/page/index.ts new file mode 100644 index 00000000..8ed95b1d --- /dev/null +++ b/packages/cli-old/src/cli/add/page/index.ts @@ -0,0 +1,230 @@ +import path from "node:path"; +import chalk from "chalk"; +import { Command } from "commander"; +import { capitalize } from "es-toolkit"; +import fs from "fs-extra"; +import { nextjsTemplates, wvTemplates } from "~/cli/add/page/templates.js"; +import * as p from "~/cli/prompts.js"; +import { PKG_ROOT } from "~/consts.js"; +import { getExistingSchemas } from "~/generators/fmdapi.js"; +import { addRouteToNav } from "~/generators/route.js"; +import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; +import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; +import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; +import { type DataSource, getSettings, mergeSettings } from "~/utils/parseSettings.js"; +import { abortIfCancel, ensureProofKitProject } from "../../utils.js"; + +export const runAddPageAction = async (opts?: { + routeName?: string; + pageName?: string; + dataSourceName?: string; + schemaName?: string; + template?: string; +}) => { + const projectDir = state.projectDir; + + const settings = getSettings(); + if (settings.ui === "shadcn") { + return p.cancel("Adding pages is not yet supported for shadcn-based projects."); + } + + const templates = state.appType === "browser" ? Object.entries(nextjsTemplates) : Object.entries(wvTemplates); + + if (templates.length === 0) { + return p.cancel("No templates found for your app type. Check back soon!"); + } + + let routeName = opts?.routeName; + let replacedMainPage = settings.replacedMainPage; + + if (state.appType === "webviewer" && !replacedMainPage && !isNonInteractiveMode() && !routeName) { + const replaceMainPage = abortIfCancel( + await p.select({ + message: "Do you want to replace the default page?", + options: [ + { label: "Yes", value: "yes" }, + { label: "No, maybe later", value: "no" }, + { label: "No, don't ask again", value: "never" }, + ], + }), + ); + if (replaceMainPage === "never" || replaceMainPage === "yes") { + replacedMainPage = true; + } + + if (replaceMainPage === "yes") { + routeName = "/"; + } + } + + if (!routeName) { + routeName = abortIfCancel( + await p.text({ + message: "Enter the URL PATH for your new page", + placeholder: "/my-page", + validate: (value) => { + if (value.length === 0) { + return "URL path is required"; + } + return; + }, + }), + ); + } + + if (!routeName.startsWith("/")) { + routeName = `/${routeName}`; + } + + const pageName = capitalize(routeName.replace("/", "").trim()); + + const template = + opts?.template ?? + abortIfCancel( + await p.select({ + message: "What template should be used for this page?", + options: templates.map(([key, value]) => ({ + value: key, + label: `${value.label}`, + hint: value.hint, + })), + }), + ); + + const pageTemplate = templates.find(([key]) => key === template)?.[1]; + if (!pageTemplate) { + return p.cancel(`Page template ${template} not found`); + } + + let dataSource: DataSource | undefined; + let schemaName: string | undefined; + if (pageTemplate.requireData) { + if (settings.dataSources.length === 0) { + return p.cancel( + "This template requires a data source, but you don't have any. Add a data source first, or choose another page template", + ); + } + + const dataSourceName = + opts?.dataSourceName ?? + (settings.dataSources.length > 1 + ? abortIfCancel( + await p.select({ + message: "Which data source should be used for this page?", + options: settings.dataSources.map((dataSource) => ({ + value: dataSource.name, + label: dataSource.name, + })), + }), + ) + : settings.dataSources[0]?.name); + + dataSource = settings.dataSources.find((dataSource) => dataSource.name === dataSourceName); + if (!dataSource) { + return p.cancel(`Data source ${dataSourceName} not found`); + } + + schemaName = await promptForSchemaFromDataSource({ + projectDir, + dataSource, + }); + } + + const spinner = p.spinner(); + spinner.start("Adding page from template"); + + // copy template files + const templatePath = path.join(PKG_ROOT, "template/pages", pageTemplate.templatePath); + + const destPath = + state.appType === "browser" + ? path.join(projectDir, "src/app/(main)", routeName) + : path.join(projectDir, "src/routes", routeName); + + await fs.copy(templatePath, destPath); + + if (state.appType === "browser") { + if (pageName && pageName !== "") { + await addRouteToNav({ + projectDir: process.cwd(), + navType: "primary", + label: pageName, + href: routeName, + }); + } + } else if (state.appType === "webviewer") { + // TODO: implement + } + // call post-install function + await pageTemplate.postIntallFn?.({ + projectDir, + pageDir: destPath, + dataSource, + schemaName, + }); + + if (replacedMainPage !== settings.replacedMainPage) { + // avoid changing this until the end since the user could cancel early + mergeSettings({ replacedMainPage }); + } + + spinner.stop("Added page!"); + const pkgManager = getUserPkgManager(); + + console.log( + `\n${chalk.green("Next steps:")}\nTo preview this page, restart your dev server using the ${chalk.cyan(`${pkgManager === "npm" ? "npm run" : pkgManager} dev`)} command\n`, + ); +}; + +export const makeAddPageCommand = () => { + const addPageCommand = new Command("page").description("Add a new page to your project").action(async () => { + await runAddPageAction(); + }); + + addPageCommand.addOption(ciOption); + addPageCommand.addOption(nonInteractiveOption); + addPageCommand.addOption(debugOption); + + addPageCommand.hook("preAction", () => { + initProgramState(addPageCommand.opts()); + state.baseCommand = "add"; + ensureProofKitProject({ commandName: "add" }); + }); + + return addPageCommand; +}; + +async function promptForSchemaFromDataSource({ + projectDir = process.cwd(), + dataSource, +}: { + projectDir?: string; + dataSource: DataSource; +}) { + if (dataSource.type === "supabase") { + throw new Error("Not implemented"); + } + const schemas = getExistingSchemas({ + projectDir, + dataSourceName: dataSource.name, + }) + .map((s) => s.schemaName) + .filter(Boolean); + + if (schemas.length === 0) { + p.cancel("This data source doesn't have any schemas to load data from"); + return undefined; + } + + if (schemas.length === 1) { + return schemas[0]; + } + + const schemaName = abortIfCancel( + await p.select({ + message: "Which schema should this page load data from?", + options: schemas.map((schema) => ({ label: schema ?? "", value: schema ?? "" })), + }), + ); + return schemaName; +} diff --git a/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts b/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts new file mode 100644 index 00000000..fcccbb27 --- /dev/null +++ b/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts @@ -0,0 +1,12 @@ +import { injectTanstackQuery } from "~/generators/tanstack-query.js"; +import { installDependencies } from "~/helpers/installDependencies.js"; +import type { TPostInstallFn } from "../types.js"; +import { postInstallTable } from "./table.js"; + +export const postInstallTableInfinite: TPostInstallFn = async (args) => { + await postInstallTable(args); + const didInject = await injectTanstackQuery(); + if (didInject) { + await installDependencies(); + } +}; diff --git a/packages/cli-old/src/cli/add/page/post-install/table.ts b/packages/cli-old/src/cli/add/page/post-install/table.ts new file mode 100644 index 00000000..a6e930ed --- /dev/null +++ b/packages/cli-old/src/cli/add/page/post-install/table.ts @@ -0,0 +1,123 @@ +import path from "node:path"; +import fs from "fs-extra"; +import { SyntaxKind } from "ts-morph"; + +import { getClientSuffix, getFieldNamesForSchema } from "~/generators/fmdapi.js"; +import { injectTanstackQuery } from "~/generators/tanstack-query.js"; +import { installDependencies } from "~/helpers/installDependencies.js"; +import { state } from "~/state.js"; +import { getSettings } from "~/utils/parseSettings.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; +import type { TPostInstallFn } from "../types.js"; + +// Regex to validate JavaScript identifiers +const VALID_JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; + +export const postInstallTable: TPostInstallFn = async ({ projectDir, pageDir, dataSource, schemaName }) => { + if (!dataSource) { + throw new Error("DataSource is required for table page"); + } + if (!schemaName) { + throw new Error("SchemaName is required for table page"); + } + if (dataSource.type !== "fm") { + throw new Error("FileMaker DataSource is required for table page"); + } + + const clientSuffix = getClientSuffix({ + projectDir, + dataSourceName: dataSource.name, + }); + + const allFieldNames = getFieldNamesForSchema({ + schemaName, + dataSourceName: dataSource.name, + }); + + const settings = getSettings(); + if (settings.ui === "shadcn") { + return; + } + const auth = settings.auth; + + const substitutions = { + __SOURCE_NAME__: dataSource.name, + __TYPE_NAME__: `T${schemaName}`, + __ZOD_TYPE_NAME__: `Z${schemaName}`, + __CLIENT_NAME__: `${schemaName}${clientSuffix}`, + __SCHEMA_NAME__: schemaName, + __ACTION_CLIENT__: auth.type === "none" ? "actionClient" : "authedActionClient", + __FIRST_FIELD_NAME__: allFieldNames[0] ?? "NO_FIELDS_ON_YOUR_LAYOUT", + }; + + // read all files in pageDir and loop over them + const files = await fs.readdir(pageDir); + for await (const file of files) { + const filePath = path.join(pageDir, file); + let fileContent = await fs.readFile(filePath, "utf8"); + + for (const [key, value] of Object.entries(substitutions)) { + fileContent = fileContent.replace(new RegExp(key, "g"), value); + } + + await fs.writeFile(filePath, fileContent, "utf8"); + } + + // add the schemas to the columns array + const project = getNewProject(projectDir); + const sourceFile = project.addSourceFileAtPath( + path.join(pageDir, state.appType === "browser" ? "table.tsx" : "index.tsx"), + ); + const columns = sourceFile.getVariableDeclaration("columns")?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); + + const fieldNames = filterOutCommonFieldNames(allFieldNames.filter(Boolean) as string[]); + + for await (const fieldName of fieldNames) { + columns?.addElement((writer) => + writer + .inlineBlock(() => { + if (needsBracketNotation(fieldName)) { + writer.write(`accessorFn: (row) => row["${fieldName}"],`); + } else { + writer.write(`accessorFn: (row) => row.${fieldName},`); + } + writer.write(`header: "${fieldName}",`); + }) + .write(",") + .newLine(), + ); + } + + if (state.appType === "webviewer") { + const didInject = await injectTanstackQuery({ project }); + if (didInject) { + await installDependencies(); + } + } + + await formatAndSaveSourceFiles(project); +}; + +// Function to check if a field name needs bracket notation +function needsBracketNotation(fieldName: string): boolean { + // Check if it's a valid JavaScript identifier + return !VALID_JS_IDENTIFIER.test(fieldName); +} + +const commonFieldNamesToExclude = [ + "id", + "pk", + "createdat", + "updatedat", + "primarykey", + "createdby", + "modifiedby", + "creationtimestamp", + "modificationtimestamp", +]; + +function filterOutCommonFieldNames(fieldNames: string[]): string[] { + return fieldNames.filter( + (fieldName) => !commonFieldNamesToExclude.includes(fieldName.toLowerCase()) || fieldName.startsWith("_"), + ); +} diff --git a/packages/cli-old/src/cli/add/page/templates.ts b/packages/cli-old/src/cli/add/page/templates.ts new file mode 100644 index 00000000..a49d5740 --- /dev/null +++ b/packages/cli-old/src/cli/add/page/templates.ts @@ -0,0 +1,85 @@ +import { postInstallTable } from "./post-install/table.js"; +import { postInstallTableInfinite } from "./post-install/table-infinite.js"; +import type { TPostInstallFn } from "./types.js"; + +export interface Template { + requireData: boolean; + label: string; + hint?: string; + templatePath: string; + screenshot?: string; + tags?: string[]; + postIntallFn?: TPostInstallFn; +} + +export const nextjsTemplates: Record = { + blank: { + requireData: false, + label: "Blank", + templatePath: "nextjs/blank", + }, + table: { + requireData: true, + label: "Basic Table", + hint: "Use to load and show multiple records", + templatePath: "nextjs/table", + postIntallFn: postInstallTable, + }, + tableEdit: { + requireData: true, + label: "Basic Table (editable)", + hint: "Use to load and show multiple records with inline edit functionality", + templatePath: "nextjs/table-edit", + postIntallFn: postInstallTable, + }, + tableInfinite: { + requireData: true, + label: "Infinite Table", + hint: "Automatically load more records when the user scrolls to the bottom", + templatePath: "nextjs/table-infinite", + postIntallFn: postInstallTableInfinite, + }, + tableInfiniteEdit: { + requireData: true, + label: "Infinite Table (editable)", + hint: "Automatically load more records when the user scrolls to the bottom with inline edit functionality", + templatePath: "nextjs/table-infinite-edit", + postIntallFn: postInstallTableInfinite, + }, +}; + +export const wvTemplates: Record = { + blank: { + requireData: false, + label: "Blank", + templatePath: "vite-wv/blank", + }, + table: { + requireData: true, + label: "Basic Table", + hint: "Use to load and show multiple records", + templatePath: "vite-wv/table", + postIntallFn: postInstallTable, + }, + tableEdit: { + requireData: true, + label: "Basic Table (editable)", + hint: "Use to load and show multiple records with inline edit functionality", + templatePath: "vite-wv/table-edit", + postIntallFn: postInstallTable, + }, + // tableInfinite: { + // requireData: true, + // label: "Infinite Table", + // hint: "Automatically load more records when the user scrolls to the bottom", + // templatePath: "vite-wv/table-infinite", + // postIntallFn: postInstallTableInfinite, + // }, + // tableInfiniteEdit: { + // requireData: true, + // label: "Infinite Table (editable)", + // hint: "Automatically load more records when the user scrolls to the bottom with inline edit functionality", + // templatePath: "vite-wv/table-infinite-edit", + // postIntallFn: postInstallTableInfinite, + // }, +}; diff --git a/packages/cli-old/src/cli/add/page/types.ts b/packages/cli-old/src/cli/add/page/types.ts new file mode 100644 index 00000000..7b7da162 --- /dev/null +++ b/packages/cli-old/src/cli/add/page/types.ts @@ -0,0 +1,19 @@ +import type { DataSource } from "~/utils/parseSettings.js"; + +export type TPostInstallFn = (args: { + projectDir: string; + /** Path in the project where the pages were copyied to. */ + pageDir: string; + dataSource?: DataSource; + schemaName?: string; +}) => void | Promise; + +export interface Template { + requireData: boolean; + label: string; + hint?: string; + /** Path from the template/pages directory to the template files to copy. */ + templatePath: string; + /** Will be run after the page contents is created and copied into the project. */ + postIntallFn?: TPostInstallFn; +} diff --git a/packages/cli-old/src/cli/add/registry/getOptions.ts b/packages/cli-old/src/cli/add/registry/getOptions.ts new file mode 100644 index 00000000..9e778b10 --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/getOptions.ts @@ -0,0 +1,44 @@ +import path from "node:path"; +import fg from "fast-glob"; +import fs from "fs-extra"; + +import { state } from "~/state.js"; +import { registryFetch } from "./http.js"; + +export async function getMetaFromRegistry(name: string) { + const result = await registryFetch("@get/meta/:name", { + params: { name }, + }); + + if (result.error) { + if (result.error.status === 404) { + return null; + } + throw new Error(result.error.message); + } + + return result.data; +} + +const PROJECT_SHARED_IGNORE = ["**/node_modules/**", ".next", "public", "dist", "build"]; + +export async function getProjectInfo() { + const cwd = state.projectDir || process.cwd(); + const [configFiles, isSrcDir] = await Promise.all([ + fg.glob("**/{next,vite,astro,app}.config.*|gatsby-config.*|composer.json|react-router.config.*", { + cwd, + deep: 3, + ignore: PROJECT_SHARED_IGNORE, + }), + fs.pathExists(path.resolve(cwd, "src")), + ]); + + const isUsingAppDir = await fs.pathExists(path.resolve(cwd, `${isSrcDir ? "src/" : ""}app`)); + + // Next.js. + if (configFiles.find((file) => file.startsWith("next.config."))?.length) { + return isUsingAppDir ? "next-app" : "next-pages"; + } + + return "manual"; +} diff --git a/packages/cli-old/src/cli/add/registry/http.ts b/packages/cli-old/src/cli/add/registry/http.ts new file mode 100644 index 00000000..5625d73b --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/http.ts @@ -0,0 +1,18 @@ +import { createFetch, createSchema } from "@better-fetch/fetch"; +import { registryIndexSchema, templateMetadataSchema } from "@proofkit/registry"; + +import { getRegistryUrl } from "~/helpers/shadcn-cli.js"; + +const schema = createSchema({ + "@get/meta/:name": { + output: templateMetadataSchema, + }, + "@get/": { + output: registryIndexSchema, + }, +}); + +export const registryFetch = createFetch({ + baseURL: `${getRegistryUrl()}/r`, + schema, +}); diff --git a/packages/cli-old/src/cli/add/registry/install.ts b/packages/cli-old/src/cli/add/registry/install.ts new file mode 100644 index 00000000..368a84a0 --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/install.ts @@ -0,0 +1,224 @@ +import { getOtherProofKitDependencies } from "@proofkit/registry"; +import { capitalize, uniq } from "es-toolkit"; +import ora from "ora"; +import semver from "semver"; +import * as p from "~/cli/prompts.js"; + +import { abortIfCancel } from "~/cli/utils.js"; +import { getExistingSchemas } from "~/generators/fmdapi.js"; +import { addRouteToNav } from "~/generators/route.js"; +import { getRegistryUrl, shadcnInstall } from "~/helpers/shadcn-cli.js"; +import { state } from "~/state.js"; +import { getVersion } from "~/utils/getProofKitVersion.js"; +import { logger } from "~/utils/logger.js"; +import { type DataSource, getSettings, mergeSettings } from "~/utils/parseSettings.js"; +import { getMetaFromRegistry } from "./getOptions.js"; +import { buildHandlebarsData, randerHandlebarsToFile } from "./postInstall/handlebars.js"; +import { processPostInstallStep } from "./postInstall/index.js"; +import { preflightAddCommand } from "./preflight.js"; + +async function promptForSchemaFromDataSource({ + projectDir = process.cwd(), + dataSource, +}: { + projectDir?: string; + dataSource: DataSource; +}) { + if (dataSource.type === "supabase") { + throw new Error("Not implemented"); + } + const schemas = getExistingSchemas({ + projectDir, + dataSourceName: dataSource.name, + }) + .map((s) => s.schemaName) + .filter(Boolean); + + if (schemas.length === 0) { + p.cancel("This data source doesn't have any schemas to load data from"); + return undefined; + } + + if (schemas.length === 1) { + return schemas[0]; + } + + const schemaName = abortIfCancel( + await p.select({ + message: "Which schema should this template use?", + options: schemas.map((schema) => ({ label: schema ?? "", value: schema ?? "" })), + }), + ); + return schemaName; +} + +export async function installFromRegistry(name: string) { + const spinner = ora("Validating template").start(); + + try { + await preflightAddCommand(); + const meta = await getMetaFromRegistry(name); + if (!meta) { + spinner.fail(`Template ${name} not found in the ProofKit registry`); + return; + } + + if (meta.minimumProofKitVersion && semver.gt(meta.minimumProofKitVersion, getVersion())) { + logger.error( + `Template ${name} requires ProofKit version ${meta.minimumProofKitVersion}, but you are using version ${getVersion()}`, + ); + spinner.fail("Template is not compatible with your ProofKit version"); + return; + } + spinner.succeed(); + + const otherProofKitDependencies = getOtherProofKitDependencies(meta); + let previouslyInstalledTemplates = getSettings().registryTemplates; + + // Handle schema requirement if template needs it + let dataSource: DataSource | undefined; + let schemaName: string | undefined; + let routeName: string | undefined; + let pageName: string | undefined; + + if (meta.schemaRequired) { + const settings = getSettings(); + + if (settings.dataSources.length === 0) { + spinner.fail("This template requires a data source, but you don't have any. Add a data source first."); + return; + } + + const dataSourceName = + settings.dataSources.length > 1 + ? abortIfCancel( + await p.select({ + message: "Which data source should be used for this template?", + options: settings.dataSources.map((ds) => ({ + value: ds.name, + label: ds.name, + })), + }), + ) + : settings.dataSources[0]?.name; + + dataSource = settings.dataSources.find((ds) => ds.name === dataSourceName); + + if (!dataSource) { + spinner.fail(`Data source ${dataSourceName} not found`); + return; + } + + schemaName = await promptForSchemaFromDataSource({ + projectDir: state.projectDir, + dataSource, + }); + + if (!schemaName) { + spinner.fail("Schema selection was cancelled"); + return; + } + } + + if (meta.category === "page") { + // Prompt user for the URL path of the page + routeName = abortIfCancel( + await p.text({ + message: "Enter the URL PATH for your new page", + placeholder: "/my-page", + validate: (value) => { + if (value.length === 0) { + return "URL path is required"; + } + return; + }, + }), + ); + + if (routeName.startsWith("/")) { + routeName = routeName.slice(1); + } + + pageName = capitalize(routeName.replace("/", "").trim()); + } + + const url = new URL(`${getRegistryUrl()}/r/${name}`); + if (meta.category === "page") { + url.searchParams.set("routeName", `/(main)/${routeName ?? name}`); + } + + // a (hopefully) temporary workaround because the shadcn command installs the env file in the wrong place if it's a dependency + if ( + name === "fmdapi" && + !previouslyInstalledTemplates.includes("utils/t3-env") && + // this last guard will allow this workaroudn to be bypassed if the registry server updates to start serving the dependency again + meta.registryDependencies?.find((d) => d.includes("utils/t3-env")) === undefined + ) { + // install the t3-env template manually first + await installFromRegistry("utils/t3-env"); + previouslyInstalledTemplates = getSettings().registryTemplates; + } + + // now install the template using shadcn-install + await shadcnInstall([url.toString()], meta.title); + + const handlebarsFiles = meta.files.filter((file) => file.handlebars); + + if (handlebarsFiles.length > 0) { + // Build template data with schema information if available + const baseTemplateData = + dataSource && schemaName + ? buildHandlebarsData({ + dataSource, + schemaName, + }) + : buildHandlebarsData(); + + // Add page information to template data if available + const templateData = { + ...baseTemplateData, + ...(routeName && { routeName }), + ...(pageName && { pageName }), + }; + + // Resolve __PATH__ placeholders in file paths before handlebars processing + const resolvedFiles = handlebarsFiles.map((file) => ({ + ...file, + destinationPath: file.destinationPath?.replace("__PATH__", `/(main)/${routeName ?? name}`), + })); + + for (const file of resolvedFiles) { + await randerHandlebarsToFile(file, templateData); + } + } + + // Add route to navigation if this is a page template + if (meta.category === "page" && routeName && pageName) { + await addRouteToNav({ + projectDir: state.projectDir, + navType: "primary", + label: pageName, + href: `/${routeName}`, + }); + } + + // if post-install steps, process those + if (meta.postInstall) { + for (const step of meta.postInstall) { + if (step._from && previouslyInstalledTemplates.includes(step._from)) { + // don't re-run post-install steps for templates that have already been installed + continue; + } + await processPostInstallStep(step); + } + } + + // update the settings + mergeSettings({ + registryTemplates: uniq([...previouslyInstalledTemplates, name, ...otherProofKitDependencies]), + }); + } catch (error) { + spinner.fail("Failed to fetch template metadata."); + logger.error(error); + } +} diff --git a/packages/cli-old/src/cli/add/registry/listItems.ts b/packages/cli-old/src/cli/add/registry/listItems.ts new file mode 100644 index 00000000..046f5c73 --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/listItems.ts @@ -0,0 +1,9 @@ +import { registryFetch } from "./http.js"; + +export async function listItems() { + const { data: items, error } = await registryFetch("@get/"); + if (error) { + throw new Error(`Failed to fetch items from registry: ${error.message}`); + } + return items; +} diff --git a/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts b/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts new file mode 100644 index 00000000..2ef0b79c --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts @@ -0,0 +1,189 @@ +import path from "node:path"; +import { decodeHandlebarsFromShadcn, type TemplateFile } from "@proofkit/registry"; +import fs from "fs-extra"; +import handlebars from "handlebars"; +import { getClientSuffix, getFieldNamesForSchema } from "~/generators/fmdapi.js"; +import { getShadcnConfig } from "~/helpers/shadcn-cli.js"; +import { state } from "~/state.js"; +import { type DataSource, getSettings } from "~/utils/parseSettings.js"; + +// Register handlebars helpers +handlebars.registerHelper("eq", (a, b) => a === b); + +interface HandlebarsContext { + [key: string]: unknown; +} + +handlebars.registerHelper("findFirst", function (this: HandlebarsContext, array, predicate, options) { + if (!(array && Array.isArray(array))) { + return options.inverse(this); + } + + for (const item of array) { + if (predicate === "fm" && item.type === "fm") { + return options.fn(item); + } + } + return options.inverse(this); +}); + +interface DataSourceForTemplate { + dataSource: DataSource; + schemaName: string; +} + +const commonFieldNamesToExclude = [ + "id", + "pk", + "createdat", + "updatedat", + "primarykey", + "createdby", + "modifiedby", + "creationtimestamp", + "modificationtimestamp", +]; + +function filterOutCommonFieldNames(fieldNames: string[]): string[] { + return fieldNames.filter( + (fieldName) => !commonFieldNamesToExclude.includes(fieldName.toLowerCase()) || fieldName.startsWith("_"), + ); +} + +function buildDataSourceData(args: DataSourceForTemplate) { + const { dataSource, schemaName } = args; + + const clientSuffix = getClientSuffix({ + projectDir: state.projectDir ?? process.cwd(), + dataSourceName: dataSource.name, + }); + + const allFieldNames = getFieldNamesForSchema({ + schemaName, + dataSourceName: dataSource.name, + }).filter(Boolean) as string[]; + + return { + sourceName: dataSource.name, + schemaName, + clientSuffix, + allFieldNames, + fieldNames: filterOutCommonFieldNames(allFieldNames), + }; +} + +export function buildHandlebarsData(args?: DataSourceForTemplate) { + const proofkit = getSettings(); + const shadcn = getShadcnConfig(); + + return { + proofkit, + shadcn, + schema: args + ? buildDataSourceData(args) + : { + sourceName: "UnknownDataSource", + schemaName: "UnknownSchema", + clientSuffix: "UnknownClientSuffix", + allFieldNames: ["UnknownFieldName"], + fieldNames: ["UnknownFieldName"], + }, + }; +} + +export async function randerHandlebarsToFile(file: TemplateFile, data: ReturnType) { + const inputPath = getFilePath(file, data); + let rawTemplate = await fs.readFile(inputPath, "utf8"); + + // Decode placeholder tokens back to handlebars syntax + // This uses the centralized decoding function from the registry package + rawTemplate = decodeHandlebarsFromShadcn(rawTemplate); + + const template = handlebars.compile(rawTemplate); + const rendered = template(data); + await fs.writeFile(inputPath, rendered); +} + +export function getFilePath(file: TemplateFile, data: ReturnType): string { + const thePath = file.sourceFileName; + + if (file.destinationPath) { + return file.destinationPath; + } + + const cwd = state.projectDir ?? process.cwd(); + const { shadcn } = data; + + // Create a mapping between registry types and their corresponding shadcn config aliases + let blockAlias = "src/components/blocks"; + if (shadcn?.aliases?.components) { + if (shadcn.aliases.components.startsWith("@/")) { + blockAlias = `${shadcn.aliases.components.replace("@/", "src/")}/blocks`; + } else { + blockAlias = `src/${shadcn.aliases.components}/blocks`; + } + } + + const typeToAliasMap: Record = { + "registry:lib": shadcn?.aliases?.lib || shadcn?.aliases?.utils, + "registry:component": shadcn?.aliases?.components, + "registry:ui": shadcn?.aliases?.ui || shadcn?.aliases?.components, + "registry:hook": shadcn?.aliases?.hooks, + // These types don't have direct aliases, so we use fallback paths + "registry:file": "src", + "registry:page": "src/app", + "registry:block": blockAlias, + "registry:theme": "src/theme", + "registry:style": "src/styles", + }; + + const aliasPath = typeToAliasMap[file.type]; + + if (aliasPath) { + // Handle @/ prefix which represents the src directory + if (aliasPath.startsWith("@/")) { + const resolvedPath = aliasPath.replace("@/", "src/"); + return path.join(cwd, resolvedPath, thePath); + } + // If the alias starts with a path separator or contains src/, treat it as a relative path from cwd + if (aliasPath.startsWith("/") || aliasPath.includes("src/")) { + return path.join(cwd, aliasPath, thePath); + } + // Otherwise, treat it as an alias that should be resolved relative to src/ + + return path.join(cwd, "src", aliasPath, thePath); + } + + // Fallback to hardcoded paths for unsupported types + switch (file.type) { + case "registry:lib": + return path.join(cwd, "src", "lib", thePath); + case "registry:file": + return path.join(cwd, "src", thePath); + case "registry:page": { + // For page templates, use the route name if available in template data + const routeName = "routeName" in data ? (data.routeName as string) : undefined; + if (routeName) { + // Add /(main) prefix for Next.js app router structure + const pageRoute = routeName === "/" ? "" : routeName; + return path.join(cwd, "src", "app", "(main)", pageRoute, thePath); + } + return path.join(cwd, "src", "app", thePath); + } + case "registry:block": + return path.join(cwd, "src", "components", "blocks", thePath); + case "registry:component": + return path.join(cwd, "src", "components", thePath); + case "registry:ui": + return path.join(cwd, "src", "components", thePath); + case "registry:hook": + return path.join(cwd, "src", "hooks", thePath); + case "registry:theme": + return path.join(cwd, "src", "theme", thePath); + case "registry:style": + return path.join(cwd, "src", "styles", thePath); + default: + // default to source file name + return thePath; + } +} diff --git a/packages/cli-old/src/cli/add/registry/postInstall/index.ts b/packages/cli-old/src/cli/add/registry/postInstall/index.ts new file mode 100644 index 00000000..6afb4233 --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/postInstall/index.ts @@ -0,0 +1,22 @@ +import type { PostInstallStep } from "@proofkit/registry"; + +import { addToEnv } from "~/utils/addToEnvs.js"; +import { logger } from "~/utils/logger.js"; +import { addScriptToPackageJson } from "./package-script.js"; +import { wrapProvider } from "./wrap-provider.js"; + +export async function processPostInstallStep(step: PostInstallStep) { + if (step.action === "package.json script") { + addScriptToPackageJson(step); + } else if (step.action === "wrap provider") { + await wrapProvider(step); + } else if (step.action === "next-steps") { + logger.info(step.data.message); + } else if (step.action === "env") { + await addToEnv({ + envs: step.data.envs, + }); + } else { + logger.error(`Unknown post-install step: ${step}`); + } +} diff --git a/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts b/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts new file mode 100644 index 00000000..50df220c --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts @@ -0,0 +1,12 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { PostInstallStep } from "@proofkit/registry"; + +import { state } from "~/state.js"; + +export function addScriptToPackageJson(step: Extract) { + const packageJsonPath = path.join(state.projectDir, "package.json"); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + packageJson.scripts[step.data.scriptName] = step.data.scriptCommand; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); +} diff --git a/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts b/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts new file mode 100644 index 00000000..dd1ec34e --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts @@ -0,0 +1,132 @@ +import path from "node:path"; +import type { PostInstallStep } from "@proofkit/registry"; +import { type ImportDeclarationStructure, type JsxChild, type JsxElement, StructureKind, SyntaxKind } from "ts-morph"; + +import { getShadcnConfig } from "~/helpers/shadcn-cli.js"; +import { state } from "~/state.js"; +import { logger } from "~/utils/logger.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; + +export async function wrapProvider(step: Extract) { + const { parentTag, imports: importConfigs, providerCloseTag, providerOpenTag } = step.data; + + try { + const projectDir = state.projectDir; + const project = getNewProject(projectDir); + const shadcnConfig = getShadcnConfig(); + + // Resolve the components alias to a filesystem path + // @/components -> src/components, ./components -> components, etc. + const resolveAlias = (alias: string): string => { + if (alias.startsWith("@/")) { + return alias.replace("@/", "src/"); + } + if (alias.startsWith("./")) { + return alias.substring(2); + } + return alias; + }; + + // Look for providers.tsx in the components directory + const componentsDir = resolveAlias(shadcnConfig.aliases.components); + const providersPath = path.join(projectDir, componentsDir, "providers.tsx"); + + const providersFile = project.addSourceFileAtPath(providersPath); + + // Add all import statements + for (const importConfig of importConfigs) { + const importDeclaration: ImportDeclarationStructure = { + moduleSpecifier: importConfig.moduleSpecifier, + kind: StructureKind.ImportDeclaration, + }; + + if (importConfig.defaultImport) { + importDeclaration.defaultImport = importConfig.defaultImport; + } + + if (importConfig.namedImports && importConfig.namedImports.length > 0) { + importDeclaration.namedImports = importConfig.namedImports; + } + + providersFile.addImportDeclaration(importDeclaration); + } + + // Handle providers.tsx file - look for the default export function + const exportDefault = providersFile.getFunction((dec) => dec.isDefaultExport()); + + if (!exportDefault) { + logger.warn(`No default export function found in ${providersPath}`); + return; + } + + const returnStatement = exportDefault?.getBody()?.getFirstDescendantByKind(SyntaxKind.ReturnStatement); + + if (!returnStatement) { + logger.warn("No return statement found in default export function"); + return; + } + + let targetElement: JsxElement | undefined; + + // Try to find the parent tag if specified + if (parentTag && parentTag.length > 0) { + for (const tag of parentTag) { + targetElement = returnStatement + ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) + .find((openingElement) => openingElement.getTagNameNode().getText() === tag) + ?.getParentIfKind(SyntaxKind.JsxElement); + + if (targetElement) { + break; + } + } + } + + if (targetElement) { + // If we found a parent tag, wrap its children + const childrenText = targetElement + ?.getJsxChildren() + .map((child: JsxChild) => child.getText()) + .filter(Boolean) + .join("\n"); + + const newContent = `${providerOpenTag} + ${childrenText} + ${providerCloseTag}`; + + targetElement.getChildSyntaxList()?.replaceWithText(newContent); + } else { + // If no parent tag found or specified, wrap the entire return statement + const returnExpression = returnStatement?.getExpression(); + if (returnExpression) { + // Check if the expression is a ParenthesizedExpression + const isParenthesized = returnExpression.getKind() === SyntaxKind.ParenthesizedExpression; + + let innerExpressionText: string; + if (isParenthesized) { + // Get the inner expression from the parenthesized expression + const parenthesizedExpr = returnExpression.asKindOrThrow(SyntaxKind.ParenthesizedExpression); + innerExpressionText = parenthesizedExpr.getExpression().getText(); + } else { + innerExpressionText = returnExpression.getText(); + } + + const newReturnContent = `return ( + ${providerOpenTag} + ${innerExpressionText} + ${providerCloseTag} + );`; + + returnStatement?.replaceWithText(newReturnContent); + } else { + logger.warn("No return expression found to wrap"); + } + } + + await formatAndSaveSourceFiles(project); + logger.success(`Successfully wrapped provider in ${providersPath}`); + } catch (error) { + logger.error(`Failed to wrap provider: ${error}`); + throw error; + } +} diff --git a/packages/cli-old/src/cli/add/registry/preflight.ts b/packages/cli-old/src/cli/add/registry/preflight.ts new file mode 100644 index 00000000..a7e973f0 --- /dev/null +++ b/packages/cli-old/src/cli/add/registry/preflight.ts @@ -0,0 +1,17 @@ +import path from "node:path"; +import fs from "fs-extra"; + +import { stealthInit } from "~/helpers/stealth-init.js"; +import { state } from "~/state.js"; + +export async function preflightAddCommand() { + const cwd = state.projectDir ?? process.cwd(); + // make sure shadcn is installed, throw if not + const shadcnInstalled = await fs.pathExists(path.join(cwd, "components.json")); + if (!shadcnInstalled) { + throw new Error("Shadcn is not installed. Please run `pnpm dlx shadcn@latest init` to install it."); + } + + // if proofkit is not inited, try to stealth init + await stealthInit(); +} diff --git a/packages/cli-old/src/cli/deploy/index.ts b/packages/cli-old/src/cli/deploy/index.ts new file mode 100644 index 00000000..ecc1deac --- /dev/null +++ b/packages/cli-old/src/cli/deploy/index.ts @@ -0,0 +1,489 @@ +import path from "node:path"; +import chalk from "chalk"; +import { Command, Option } from "commander"; +import { execa } from "execa"; +import fs from "fs-extra"; +import type { PackageJson } from "type-fest"; +import * as p from "~/cli/prompts.js"; + +import { ciOption, debugOption } from "~/globalOptions.js"; + +// Regex patterns defined at top level for performance +const LEADING_SYMBOLS_REGEX = /^[✔\s]+/; +const MULTI_SPACE_REGEX = /\s{2,}/; +const VERSION_PREFIX_REGEX = /^v/; + +import { initProgramState, state } from "~/state.js"; +import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; +import { getSettings } from "~/utils/parseSettings.js"; +import { ensureProofKitProject } from "../utils.js"; + +async function checkVercelCLI(): Promise { + try { + await execa("vercel", ["--version"]); + return true; + } catch (_error) { + return false; + } +} + +async function installVercelCLI() { + const pkgManager = getUserPkgManager(); + const spinner = p.spinner(); + spinner.start("Installing Vercel CLI..."); + + try { + const installCmd = pkgManager === "npm" ? "install" : "add"; + await execa(pkgManager, [installCmd, "-g", "vercel"]); + spinner.stop("Vercel CLI installed successfully"); + return true; + } catch (error) { + spinner.stop("Failed to install Vercel CLI"); + console.error(chalk.red("Error installing Vercel CLI:"), error); + return false; + } +} + +async function checkVercelProject(): Promise { + try { + // Try to read the .vercel/project.json file which exists when a project is linked + const projectConfig = (await fs.readJSON(".vercel/project.json")) as VercelProjectConfig; + return Boolean(projectConfig.projectId); + } catch (_error) { + if (state.debug) { + console.log("\nDebug: No Vercel project configuration found"); + } + return false; + } +} + +async function getVercelTeams(): Promise<{ slug: string; name: string }[]> { + try { + if (state.debug) { + console.log("\nDebug: Running vercel teams list command..."); + } + + const result = await execa("vercel", ["teams", "list"], { + all: true, + }); + + if (state.debug) { + console.log("\nDebug: Command output:", result.all); + } + + const lines = (result.all ?? "").split("\n").filter(Boolean); + + // Find the index of the header line + const headerIndex = lines.findIndex((line) => line.includes("id")); + if (headerIndex === -1) { + return []; + } + + // Get only the lines after the header + const teamLines = lines.slice(headerIndex + 1); + + if (state.debug) { + console.log("\nDebug: Team lines:"); + for (const line of teamLines) { + console.log(`"${line}"`); + } + } + + const teams = teamLines + .map((line) => { + // Remove any leading symbols (✔ or spaces) and trim + const cleanLine = line.replace(LEADING_SYMBOLS_REGEX, "").trim(); + // Split on multiple spaces and take the first part as slug, rest as name + const [slug, ...nameParts] = cleanLine.split(MULTI_SPACE_REGEX); + if (!slug || nameParts.length === 0) { + return null; + } + + return { + slug, + name: nameParts.join(" ").trim(), + }; + }) + .filter((team): team is { slug: string; name: string } => team !== null); + + if (state.debug) { + console.log("\nDebug: Parsed teams:", teams); + } + + return teams; + } catch (error) { + if (state.debug) { + console.error("Error getting Vercel teams:", error); + } + return []; + } +} + +async function setupVercelProject() { + const spinner = p.spinner(); + + try { + // Get project name from package.json + const pkgJson = (await fs.readJSON("package.json")) as PackageJson; + const projectName = pkgJson.name; + + // Get available teams + const teams = await getVercelTeams(); + + let teamFlag = ""; + if (teams.length > 1) { + const teamChoice = await p.select({ + message: "Select a team to deploy under:", + options: [ + ...teams.map((team) => ({ + value: team.slug, + label: team.name, + })), + ], + }); + + if (p.isCancel(teamChoice)) { + console.log(chalk.yellow("\nOperation cancelled")); + return false; + } + + if (teamChoice && typeof teamChoice === "string") { + teamFlag = `--scope=${teamChoice}`; + } + } + + spinner.start("Creating Vercel project..."); + + // Create project with default settings + await execa("vercel", ["link", "--yes", ...(teamFlag ? [teamFlag] : [])], { + env: { + VERCEL_PROJECT_NAME: projectName, + }, + }); + + // Pull project settings + spinner.message("Pulling project settings..."); + await execa("vercel", ["pull", "--yes"], { + stdio: "inherit", + }); + + spinner.stop("Vercel project created successfully"); + return true; + } catch (error) { + spinner.stop("Failed to set up Vercel project"); + console.error(chalk.red("Error setting up Vercel project:"), error); + return false; + } +} + +async function pushEnvironmentVariables() { + const spinner = p.spinner(); + spinner.start("Pushing environment variables to Vercel..."); + + try { + const settings = getSettings(); + const envFile = path.join(process.cwd(), settings.envFile ?? ".env"); + + if (!fs.existsSync(envFile)) { + spinner.stop("No environment file found"); + return true; + } + + const envContent = await fs.readFile(envFile, "utf-8"); + const envVars = envContent + .split("\n") + .filter((line) => line.trim() && !line.startsWith("#")) + .map((line) => { + const [key, ...valueParts] = line.split("="); + if (!key) { + return null; + } + const value = valueParts.join("="); // Rejoin in case value contains = + return { key: key.trim(), value: value.trim() }; + }) + .filter((item): item is { key: string; value: string } => item !== null); + + if (state.debug) { + spinner.stop(); + console.log("\nDebug: Parsed environment variables:"); + for (const { key, value } of envVars) { + console.log(` ${key}=${value.substring(0, 3)}...`); + } + spinner.start("Pushing environment variables to Vercel..."); + } + + let failed = 0; + const total = envVars.length; + + for (let i = 0; i < total; i++) { + const envVar = envVars[i]; + if (!envVar) { + continue; + } + const { key, value } = envVar; + spinner.message(`Pushing environment variables to Vercel... (${i + 1}/${total})`); + + try { + if (state.debug) { + console.log(`\nDebug: Attempting to add ${key} to Vercel...`); + } + + const result = await execa("vercel", ["env", "add", key, "production"], { + input: value, + stdio: "pipe", + reject: false, + }); + + if (state.debug) { + console.log(`Debug: Command exit code: ${result.exitCode}`); + if (result.stdout) { + console.log("Debug: stdout:", result.stdout); + } + if (result.stderr) { + console.log("Debug: stderr:", result.stderr); + } + } + + if (result.exitCode !== 0) { + throw new Error(`Command failed with exit code ${result.exitCode}`); + } + } catch (error) { + failed++; + if (state.debug) { + console.error(chalk.yellow(`\nDebug: Failed to add ${key}`)); + console.error("Debug: Full error:", error); + } + } + } + + if (failed > 0) { + spinner.stop(chalk.yellow(`Environment variables pushed with ${failed} failures`)); + } else { + spinner.stop("Environment variables pushed successfully"); + } + return failed < total; + } catch (error) { + spinner.stop("Failed to push environment variables"); + if (state.debug) { + console.error("\nDebug: Top-level error in pushEnvironmentVariables:"); + console.error(error); + } + return false; + } +} + +interface VercelProjectConfig { + projectId: string; + settings?: { + nodeVersion?: string; + }; + [key: string]: unknown; +} + +async function ensureCorrectNodeVersion() { + const nodeVersion = process.version.replace(VERSION_PREFIX_REGEX, ""); + const majorVersion = nodeVersion.split(".")[0]; + + try { + const projectJsonPath = ".vercel/project.json"; + if (!fs.existsSync(projectJsonPath)) { + if (state.debug) { + console.log("Debug: No project.json found"); + } + return false; + } + + const projectConfig = (await fs.readJSON(projectJsonPath)) as VercelProjectConfig; + if (state.debug) { + console.log("Debug: Current project config:", projectConfig); + } + + // Update the Node.js version + projectConfig.settings = { + ...projectConfig.settings, + nodeVersion: `${majorVersion}.x`, + }; + + await fs.writeJSON(projectJsonPath, projectConfig, { spaces: 2 }); + if (state.debug) { + console.log(`Debug: Updated Node.js version to ${majorVersion}.x`); + } + return true; + } catch (error) { + if (state.debug) { + console.error("Debug: Failed to update Node.js version:", error); + } + return false; + } +} + +async function checkVercelLogin(): Promise { + try { + const result = await execa("vercel", ["whoami"], { + stdio: "pipe", + reject: false, + }); + + if (state.debug) { + console.log("\nDebug: Vercel whoami result:", result); + } + + return result.exitCode === 0; + } catch (error) { + if (state.debug) { + console.error("Debug: Error checking Vercel login status:", error); + } + return false; + } +} + +async function loginToVercel(): Promise { + console.log(chalk.blue("\nYou need to log in to Vercel first.")); + + try { + await execa("vercel", ["login"], { + stdio: "inherit", + }); + return true; + } catch (error) { + console.error(chalk.red("\nFailed to log in to Vercel:"), error); + return false; + } +} + +export async function runDeploy() { + if (state.debug) { + console.log("Running deploy..."); + } + + // Check if Vercel CLI is installed + const hasVercelCLI = await checkVercelCLI(); + + if (!hasVercelCLI) { + const installed = await installVercelCLI(); + if (!installed) { + console.log(chalk.red("\nFailed to install Vercel CLI. Please install it manually using:")); + console.log(chalk.blue("\n npm install -g vercel")); + return; + } + } + + // Check if user is logged in + const isLoggedIn = await checkVercelLogin(); + if (!isLoggedIn) { + const loginSuccessful = await loginToVercel(); + if (!loginSuccessful) { + console.log(chalk.red("\nFailed to log in to Vercel. Please try again.")); + return; + } + } + + // Check if project is set up with Vercel + const hasVercelProject = await checkVercelProject(); + + if (!hasVercelProject) { + console.log(chalk.blue("\nSetting up new Vercel project...")); + const setup = await setupVercelProject(); + if (!setup) { + console.log(chalk.red("\nFailed to set up Vercel project automatically.")); + return; + } + + const envPushed = await pushEnvironmentVariables(); + if (!envPushed) { + console.log(chalk.red("\nFailed to push environment variables. Aborting deployment.")); + return; + } + } + + // Pull latest project settings + console.log(chalk.blue("\nPulling latest project settings...")); + try { + await execa("vercel", ["pull", "--yes"], { + stdio: "inherit", + }); + } catch (error) { + console.error(chalk.red("\nFailed to pull project settings:"), error); + return; + } + + // Ensure correct Node.js version is set + if (!(await ensureCorrectNodeVersion())) { + console.error(chalk.red("\nFailed to set Node.js version. Continuing anyway...")); + } + + if (state.localBuild) { + // Build locally for Vercel + console.log(chalk.blue("\nPreparing local build for Vercel...")); + try { + const result = await execa("vercel", ["build"], { + stdio: "inherit", + reject: false, + }); + if (result.exitCode === 0) { + console.log(chalk.green("\n✓ Local build successful!")); + } else { + console.error(chalk.red("\n✖ Local build failed")); + console.log(chalk.yellow("Fix the errors above and then try again.")); + return; + } + } catch (error) { + console.error(chalk.red("\nVercel build failed:"), error); + return; + } + + // Deploy the pre-built project + console.log(chalk.blue("\nDeploying to Vercel...")); + + const result = await execa("vercel", ["deploy", "--prebuilt", "--yes"], { + stdio: "inherit", + reject: false, + }); + if (result.exitCode === 0) { + console.log(chalk.green("\n✓ Deployment successful!")); + } + } else { + // Deploy and build on Vercel + console.log(chalk.blue("\nDeploying to Vercel...")); + try { + const result = await execa("vercel", ["deploy", "--yes"], { + stdio: "inherit", + reject: false, + }); + if (result.exitCode === 0) { + console.log(chalk.green("\n✓ Deployment successful!")); + } else { + const pkgManager = getUserPkgManager(); + const runCmd = pkgManager === "npm" ? "npm run" : pkgManager; + console.error(chalk.red("\n✖ Deployment failed")); + + console.log(chalk.yellow("\nTroubleshooting Tips:")); + console.log(chalk.dim("You can check for most errors before deploying for a faster iteration cycle")); + console.log( + `${chalk.dim("Run")} ${runCmd} tsc ${chalk.dim("to check for TypeScript errors (most common build errors)")}`, + ); + console.log(`${chalk.dim("Run")} ${runCmd} build ${chalk.dim("to run the full production build locally")}`); + } + } catch { + // This catch block should rarely be hit since we're using reject: false + return; + } + } +} + +export const makeDeployCommand = () => { + const deployCommand = new Command("deploy") + .description("Deploy your ProofKit application to Vercel") + .addOption(ciOption) + .addOption(debugOption) + .addOption(new Option("--local-build", "Build locally before deploying")) + .action(runDeploy); + + deployCommand.hook("preAction", (thisCommand) => { + initProgramState(thisCommand.opts()); + state.baseCommand = "deploy"; + ensureProofKitProject({ commandName: "deploy" }); + }); + + return deployCommand; +}; diff --git a/packages/cli-old/src/cli/fmdapi.ts b/packages/cli-old/src/cli/fmdapi.ts new file mode 100644 index 00000000..cb252b8a --- /dev/null +++ b/packages/cli-old/src/cli/fmdapi.ts @@ -0,0 +1,57 @@ +import DataApi, { type clientTypes, OttoAdapter, type OttoAPIKey } from "@proofkit/fmdapi"; + +export async function getLayouts({ + dataApiKey, + fmFile, + server, +}: { + dataApiKey: OttoAPIKey; + fmFile: string; + server: string; +}) { + const DapiClient = DataApi({ + adapter: new OttoAdapter({ + auth: { apiKey: dataApiKey }, + db: fmFile, + server, + }), + layout: "", + }); + + const layoutsResp = await DapiClient.layouts(); + + const layouts = transformLayoutList(layoutsResp.layouts); + + return layouts; +} + +function getAllLayoutNames(layout: clientTypes.LayoutOrFolder): string[] { + if ("isFolder" in layout) { + return (layout.folderLayoutNames ?? []).flatMap(getAllLayoutNames); + } + return [layout.name]; +} + +export const commonFileMakerLayoutPrefixes = ["API_", "API ", "dapi_", "dapi"]; + +export function transformLayoutList(layouts: clientTypes.LayoutOrFolder[]): string[] { + const flatList = layouts.flatMap(getAllLayoutNames); + + // sort the list so that any values that begin with one of the prefixes are at the top + + const sortedList = flatList.sort((a, b) => { + const aPrefix = commonFileMakerLayoutPrefixes.find((prefix) => a.startsWith(prefix)); + const bPrefix = commonFileMakerLayoutPrefixes.find((prefix) => b.startsWith(prefix)); + if (aPrefix && bPrefix) { + return a.localeCompare(b); + } + if (aPrefix) { + return -1; + } + if (bPrefix) { + return 1; + } + return a.localeCompare(b); + }); + return sortedList; +} diff --git a/packages/cli-old/src/cli/init.ts b/packages/cli-old/src/cli/init.ts new file mode 100644 index 00000000..4b8cc21c --- /dev/null +++ b/packages/cli-old/src/cli/init.ts @@ -0,0 +1,395 @@ +import path from "node:path"; +import { Command } from "commander"; +import { execa } from "execa"; +import fs from "fs-extra"; +import type { PackageJson } from "type-fest"; + +import { DEFAULT_APP_NAME } from "~/consts.js"; +import { addAuth } from "~/generators/auth.js"; +import { runCodegenCommand } from "~/generators/fmdapi.js"; +import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; +import { createBareProject } from "~/helpers/createProject.js"; +import { initializeGit } from "~/helpers/git.js"; +import { installDependencies } from "~/helpers/installDependencies.js"; +import { logNextSteps } from "~/helpers/logNextSteps.js"; +import { setImportAlias } from "~/helpers/setImportAlias.js"; +import { buildPkgInstallerMap } from "~/installers/index.js"; +import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; +import { getVersion } from "~/utils/getProofKitVersion.js"; +import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; +import { parseNameAndPath } from "~/utils/parseNameAndPath.js"; +import { type Settings, setSettings } from "~/utils/parseSettings.js"; +import { validateAppName } from "~/utils/validateAppName.js"; +import { promptForFileMakerDataSource } from "./add/data-source/filemaker.js"; +import { select, text } from "./prompts.js"; +import { abortIfCancel } from "./utils.js"; + +interface CliFlags { + noGit: boolean; + noInstall: boolean; + force: boolean; + default: boolean; + importAlias: string; + server?: string; + adminApiKey?: string; + fileName: string; + layoutName: string; + schemaName: string; + dataApiKey: string; + fmServerURL: string; + auth: "none" | "next-auth" | "clerk"; + dataSource?: "filemaker" | "none" | "supabase"; + /** @internal UI library selection; hidden flag */ + ui?: "shadcn" | "mantine"; + /** @internal Used in CI. */ + CI: boolean; + /** @internal Used in non-interactive mode. */ + nonInteractive?: boolean; + /** @internal Used in CI. */ + tailwind: boolean; + /** @internal Used in CI. */ + trpc: boolean; + /** @internal Used in CI. */ + prisma: boolean; + /** @internal Used in CI. */ + drizzle: boolean; + /** @internal Used in CI. */ + appRouter: boolean; +} + +const defaultOptions: CliFlags = { + noGit: false, + noInstall: false, + force: false, + default: false, + CI: false, + tailwind: false, + trpc: false, + prisma: false, + drizzle: false, + importAlias: "~/", + appRouter: false, + auth: "none", + server: undefined, + fileName: "", + layoutName: "", + schemaName: "", + dataApiKey: "", + fmServerURL: "", + dataSource: undefined, + ui: "shadcn", +}; + +export const makeInitCommand = () => { + const initCommand = new Command("init") + .description("Create a new project with ProofKit") + .argument("[dir]", "The name of the application, as well as the name of the directory to create") + .option("--appType [type]", "The type of app to create", undefined) + // hidden UI selector; default is shadcn; pass --ui mantine to opt-in legacy Mantine templates + .option("--ui [ui]", undefined, undefined) + .option("--server [url]", "The URL of your FileMaker Server", undefined) + .option("--adminApiKey [key]", "Admin API key for OttoFMS. If provided, will skip login prompt", undefined) + .option("--fileName [name]", "The name of the FileMaker file to use for the web app", undefined) + .option("--layoutName [name]", "The name of the FileMaker layout to use for the web app", undefined) + .option("--schemaName [name]", "The name for the generated layout client in your schemas", undefined) + .option("--dataApiKey [key]", "The API key to use for the FileMaker Data API", undefined) + .option("--auth [type]", "The authentication provider to use for the web app", undefined) + .option("--dataSource [type]", "The data source to use for the web app (filemaker or none)", undefined) + .option("--noGit", "Explicitly tell the CLI to not initialize a new git repo in the project", false) + .option("--noInstall", "Explicitly tell the CLI to not run the package manager's install command", false) + .option("-f, --force", "Force overwrite target directory when it already contains files", false) + .addOption(ciOption) + .addOption(nonInteractiveOption) + .addOption(debugOption) + .action(runInit); + + initCommand.hook("preAction", (cmd) => { + initProgramState(cmd.opts()); + state.baseCommand = "init"; + }); + + return initCommand; +}; + +async function askForAuth({ projectDir }: { projectDir: string }) { + const authType = "none" as "none" | "clerk" | "fmaddon"; + if (authType === "clerk") { + await addAuth({ + options: { type: "clerk" }, + projectDir, + noInstall: true, + }); + } else if (authType === "fmaddon") { + await addAuth({ + options: { type: "fmaddon" }, + projectDir, + noInstall: true, + }); + } +} + +type ProofKitPackageJSON = PackageJson & { + proofkitMetadata?: { + initVersion: string; + }; +}; + +const missingTypegenCommandPatterns = [ + /ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL[\s\S]*Command\s+["'`]typegen["'`]\s+not found/i, + /Command\s+["'`]typegen["'`]\s+not found/i, + /Missing script:\s*["'`]typegen["'`]/i, + /Script not found\s*["'`]typegen["'`]/i, +]; + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +export function isMissingTypegenCommandError(error: unknown): boolean { + const message = getErrorMessage(error); + return missingTypegenCommandPatterns.some((pattern) => pattern.test(message)); +} + +export function createPostInitGenerationError({ + error, + appType, + projectDir, +}: { + error: unknown; + appType: "browser" | "webviewer"; + projectDir: string; +}) { + const rootError = error instanceof Error ? error : new Error(getErrorMessage(error)); + + if (appType === "browser" && isMissingTypegenCommandError(error)) { + return new Error( + [ + "Post-init generation failed after scaffolding.", + `Project created at: ${projectDir}`, + "Root cause: a `typegen` package command was invoked, but browser scaffolds do not define that script.", + "Continue using the generated project, then run `proofkit typegen` later after FileMaker setup is complete.", + ].join("\n"), + { cause: rootError }, + ); + } + + return new Error( + [ + "Post-init generation failed after scaffolding.", + `Project created at: ${projectDir}`, + "Retry `proofkit typegen` from inside the project once FileMaker settings and connectivity are valid.", + `Underlying error: ${getErrorMessage(error)}`, + ].join("\n"), + { cause: rootError }, + ); +} + +export const runInit = async (name?: string, opts?: CliFlags) => { + const pkgManager = getUserPkgManager(); + const cliOptions = opts ?? defaultOptions; + const nonInteractive = isNonInteractiveMode(); + const noInstall = cliOptions.noInstall ?? (opts as { install?: boolean } | undefined)?.install === false; + const noGit = cliOptions.noGit ?? (opts as { git?: boolean } | undefined)?.git === false; + // capture ui choice early into state + state.ui = (cliOptions.ui ?? "shadcn") as "shadcn" | "mantine"; + + let projectName = name; + if (!projectName) { + if (nonInteractive) { + throw new Error("Project name is required in non-interactive mode."); + } + projectName = abortIfCancel( + await text({ + message: "What will your project be called?", + defaultValue: DEFAULT_APP_NAME, + validate: validateAppName, + }), + ).toString(); + } + + const appNameValidation = validateAppName(projectName); + if (appNameValidation) { + throw new Error(appNameValidation); + } + + const hasExplicitFileMakerInputs = Boolean( + cliOptions.server || + cliOptions.adminApiKey || + cliOptions.dataApiKey || + cliOptions.fileName || + cliOptions.layoutName || + cliOptions.schemaName, + ); + const hasPartialFileMakerSchemaInputs = Boolean(cliOptions.layoutName) !== Boolean(cliOptions.schemaName); + + if (!state.appType) { + state.appType = nonInteractive + ? "browser" + : (abortIfCancel( + await select({ + message: "What kind of app do you want to build?", + options: [ + { + value: "browser", + label: "Web App for Browsers", + hint: "Uses Next.js, will require hosting", + }, + { + value: "webviewer", + label: "FileMaker Web Viewer (beta)", + hint: "Uses Vite, can be embedded in FileMaker or hosted", + }, + ], + }), + ) as "browser" | "webviewer"); + } + + if (nonInteractive && hasPartialFileMakerSchemaInputs) { + throw new Error("Both --layoutName and --schemaName must be provided together."); + } + + if (nonInteractive && hasExplicitFileMakerInputs) { + const resolvedDataSourceForValidation = + state.appType === "webviewer" + ? (cliOptions.dataSource ?? (cliOptions.server ? "filemaker" : "none")) + : (cliOptions.dataSource ?? "none"); + + if (resolvedDataSourceForValidation !== "filemaker") { + throw new Error("FileMaker flags require --dataSource filemaker in non-interactive mode."); + } + } + + const usePackages = buildPkgInstallerMap(); + + // e.g. dir/@mono/app returns ["@mono/app", "dir/app"] + const [scopedAppName, appDir] = parseNameAndPath(projectName); + + const projectDir = await createBareProject({ + projectName: appDir, + scopedAppName, + packages: usePackages, + noInstall, + force: cliOptions.force, + appRouter: cliOptions.appRouter, + }); + setImportAlias(projectDir, "@/"); + + // Write name to package.json + const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as ProofKitPackageJSON; + pkgJson.name = scopedAppName; + pkgJson.proofkitMetadata = { initVersion: getVersion() }; + + // ? Bun doesn't support this field (yet) + if (pkgManager !== "bun") { + const { stdout } = await execa(pkgManager, ["-v"], { + cwd: projectDir, + }); + pkgJson.packageManager = `${pkgManager}@${stdout.trim()}`; + } + + fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, { + spaces: 2, + }); + + // Ensure proofkit.json exists with initial settings including ui + const initialSettings: Settings = + state.ui === "mantine" + ? { + appType: state.appType ?? "browser", + ui: "mantine", + auth: { type: "none" }, + envFile: ".env", + dataSources: [], + tanstackQuery: false, + replacedMainPage: false, + appliedUpgrades: [], + reactEmail: false, + reactEmailServer: false, + registryTemplates: [], + } + : { + appType: state.appType ?? "browser", + ui: "shadcn", + envFile: ".env", + dataSources: [], + replacedMainPage: false, + registryTemplates: [], + }; + setSettings(initialSettings); + + // for webviewer apps FM is required, so don't ask + let dataSource = + state.appType === "webviewer" + ? (cliOptions.dataSource ?? (nonInteractive && !cliOptions.server ? "none" : "filemaker")) + : (cliOptions.dataSource ?? (nonInteractive ? "none" : undefined)); + if (!dataSource) { + dataSource = abortIfCancel( + await select({ + message: "Do you want to connect to a FileMaker Database now?", + options: [ + { + value: "filemaker", + label: "Yes", + hint: "Requires OttoFMS and Admin Server credentials", + }, + // { value: "supabase", label: "Supabase" }, + { + value: "none", + label: "No", + hint: "You'll be able to add a new data source later", + }, + ], + }), + ) as "filemaker" | "none" | "supabase"; + } + + if (dataSource === "filemaker") { + // later will split this flow to ask for which kind of data souce, but for now it's just FM + await promptForFileMakerDataSource({ + projectDir, + name: "filemaker", + adminApiKey: cliOptions.adminApiKey, + dataApiKey: cliOptions.dataApiKey, + server: cliOptions.server, + fileName: cliOptions.fileName, + layoutName: cliOptions.layoutName, + schemaName: cliOptions.schemaName, + }); + } else if (dataSource === "supabase") { + // TODO: add supabase + } + + await askForAuth({ projectDir }); + + if (!noInstall) { + await installDependencies({ projectDir }); + } + + if (dataSource === "filemaker") { + const shouldRunInitialCodegen = state.appType === "webviewer" && !(nonInteractive && !hasExplicitFileMakerInputs); + + if (shouldRunInitialCodegen) { + try { + await runCodegenCommand(); + } catch (error) { + throw createPostInitGenerationError({ + error, + appType: state.appType ?? "browser", + projectDir, + }); + } + } + } + + if (!noGit) { + await initializeGit(projectDir); + } + + logNextSteps({ + projectName: appDir, + noInstall, + }); +}; diff --git a/packages/cli-old/src/cli/menu.ts b/packages/cli-old/src/cli/menu.ts new file mode 100644 index 00000000..be40d6a7 --- /dev/null +++ b/packages/cli-old/src/cli/menu.ts @@ -0,0 +1,102 @@ +import chalk from "chalk"; +import open from "open"; +import { confirm, log, select } from "~/cli/prompts.js"; + +import { DOCS_URL } from "~/consts.js"; +import { checkForAvailableUpgrades, runAllAvailableUpgrades } from "~/upgrades/index.js"; +import { getSettings } from "~/utils/parseSettings.js"; +import { runAdd } from "./add/index.js"; +import { runDeploy } from "./deploy/index.js"; +import { runRemove } from "./remove/index.js"; +import { runTypegen } from "./typegen/index.js"; +import { runUpgrade } from "./update/index.js"; +import { abortIfCancel } from "./utils.js"; + +export const runMenu = async () => { + const settings = getSettings(); + const upgrades = checkForAvailableUpgrades(); + + if (upgrades.length > 0) { + log.info( + `${chalk.yellow("There are upgrades available for your ProofKit project")}\n${upgrades + .map((upgrade) => `- ${upgrade.title}`) + .join("\n")}`, + ); + + const shouldRunUpgrades = abortIfCancel( + await confirm({ + message: "Would you like to run them now?", + initialValue: true, + }), + ); + + if (shouldRunUpgrades) { + await runAllAvailableUpgrades(); + log.success(chalk.green("Successfully ran all upgrades")); + } else { + log.info(`You can apply the upgrades later by running ${chalk.cyan("proofkit upgrade")}`); + } + } + + const menuChoice = abortIfCancel( + await select({ + message: "What would you like to do?", + options: [ + { + label: "Add Components", + value: "add", + hint: "Add new pages, schemas, data sources, etc.", + }, + { + label: "Remove Components", + value: "remove", + hint: "Remove pages, schemas, data sources, etc.", + }, + { + label: "Generate Types", + value: "typegen", + hint: "Update field definitions from your data sources", + }, + { + label: "Deploy", + value: "deploy", + hint: "Deploy your app to Vercel", + }, + { + label: "Upgrade Components", + value: "upgrade", + hint: "Update ProofKit components to latest version", + }, + { + label: "View Documentation", + value: "docs", + hint: "Open ProofKit documentation", + }, + ], + }), + ); + + switch (menuChoice) { + case "add": + await runAdd(undefined); + break; + case "remove": + await runRemove(undefined); + break; + case "docs": + log.info(`Opening ${chalk.cyan(DOCS_URL)} in your browser...`); + await open(DOCS_URL); + break; + case "typegen": + await runTypegen({ settings }); + break; + case "deploy": + await runDeploy(); + break; + case "upgrade": + await runUpgrade(); + break; + default: + throw new Error(`Unknown menu choice: ${menuChoice}`); + } +}; diff --git a/packages/cli-old/src/cli/ottofms.ts b/packages/cli-old/src/cli/ottofms.ts new file mode 100644 index 00000000..569ad348 --- /dev/null +++ b/packages/cli-old/src/cli/ottofms.ts @@ -0,0 +1,268 @@ +import axios, { AxiosError } from "axios"; +import chalk from "chalk"; +import open from "open"; +import randomstring from "randomstring"; +import { z } from "zod/v4"; + +import * as clack from "~/cli/prompts.js"; +import { abortIfCancel } from "./utils.js"; + +interface WizardResponse { + token: string; +} +export async function getOttoFMSToken({ url }: { url: URL }): Promise<{ token: string }> { + // generate a random string + const hash = randomstring.generate({ length: 18, charset: "alphanumeric" }); + + const loginUrl = new URL(`/otto/wizard/${hash}`, url.origin); + + const urlToOpen = loginUrl.toString(); + clack.log.info( + `${chalk.bold( + `If the browser window didn't open automatically, please open the following link to login into your OttoFMS server:`, + )}\n\n${chalk.cyan(urlToOpen)}`, + ); + + open(loginUrl.toString()).catch(() => { + // Ignore errors from open() - the user can manually open the URL + }); + + const loginSpinner = clack.spinner(); + + loginSpinner.start("Waiting for you to log in using the link above"); + + const data = await new Promise((resolve) => { + const pollingInterval = setInterval(() => { + axios + .get<{ response: WizardResponse }>(`${url.origin}/otto/api/cli/checkHash/${hash}`, { + headers: { + "Accept-Encoding": "deflate", + }, + }) + .then((result) => { + resolve(result.data.response); + clearTimeout(timeout); + clearInterval(pollingInterval); + axios + .delete(`${url.origin}/otto/api/cli/checkHash/${hash}`, { + headers: { + "Accept-Encoding": "deflate", + }, + }) + .catch(() => { + // Ignore cleanup errors + }); + }) + .catch(() => { + // noop - just try again + }); + }, 500); + + const timeout = setTimeout(() => { + clearInterval(pollingInterval); + loginSpinner.stop("Login timed out. No worries - it happens to the best of us."); + }, 180_000); // 3 minutes + }); + // clack.log.info(`Token: ${JSON.stringify(data)}`); + + loginSpinner.stop("Login complete."); + + return data; +} + +interface ListFilesResponse { + response: { + databases: { + clients: number; + decryptHint: string; + enabledExtPrivileges: string[]; + filename: string; + folder: string; + hasSavedDecryptKey: boolean; + id: string; + isEncrypted: boolean; + size: number; + status: string; + }[]; + }; +} + +export async function listFiles({ url, token }: { url: URL; token: string }) { + const response = await axios.get(`${url.origin}/otto/fmi/admin/api/v2/databases`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return response.data.response.databases; +} + +interface ListAPIKeysResponse { + response: { + "api-keys": { + id: number; + key: string; + token: string; + user: string; + database: string; + label: string; + created_at: string; + updated_at: string; + }[]; + }; +} + +export async function listAPIKeys({ url, token }: { url: URL; token: string }) { + const response = await axios.get(`${url.origin}/otto/api/api-key`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return response.data.response["api-keys"]; +} + +interface CreateAPIKeyResponse { + response: { + key: string; + token: string; + }; +} +export async function createDataAPIKey({ url, filename }: { url: URL; filename: string }) { + clack.log.info( + `${chalk.cyan("Creating a Data API Key")}\nEnter FileMaker credentials for ${chalk.bold(filename)}.\n${chalk.dim("The account must have the fmrest extended privilege enabled.")}`, + ); + + while (true) { + const username = abortIfCancel( + await clack.text({ + message: `Enter the account name for ${chalk.bold(filename)}`, + }), + ); + + const password = abortIfCancel( + await clack.password({ + message: `Enter the password for ${chalk.bold(username)}`, + }), + ); + + try { + const response = await createDataAPIKeyWithCredentials({ + url, + filename, + username, + password, + }); + + return response; + } catch (error) { + if (error instanceof AxiosError) { + const respMsg = + error.response?.data && "messages" in error.response.data + ? (error.response.data as { messages?: { text?: string }[] }).messages?.[0]?.text + : undefined; + + clack.log.error( + `${chalk.red("Error creating Data API key:")} ${respMsg ?? `Error code ${error.response?.status}`} +${chalk.dim( + error.response?.status === 400 && + `Common reasons this might happen: +- The provided credentials are incorrect. +- The account does not have the fmrest extended privilege enabled. + +You may also want to try to create an API directly in the OttoFMS dashboard: +${url.origin}/otto/app/api-keys`, +)} + `, + ); + } else { + clack.log.error(`${chalk.red("Error creating Data API key:")} Unknown error`); + } + const tryAgain = abortIfCancel( + await clack.confirm({ + message: "Do you want to try and enter credentials again?", + active: "Yes, try again", + inactive: "No, abort", + }), + ); + if (!tryAgain) { + throw new Error("User cancelled"); + } + } + } +} + +export async function createDataAPIKeyWithCredentials({ + url, + filename, + username, + password, +}: { + url: URL; + filename: string; + username: string; + password: string; +}) { + const response = await axios.post(`${url.origin}/otto/api/api-key/create-only`, { + database: filename, + label: "For FM Web App", + user: username, + pass: password, + }); + + return { apiKey: response.data.response.key }; +} + +export async function startDeployment({ payload, url, token }: { payload: unknown; url: URL; token: string }) { + const responseSchema = z.object({ + response: z.object({ + started: z.boolean(), + batchId: z.number(), + subDeploymentIds: z.array(z.number()), + }), + messages: z.array(z.object({ code: z.number(), text: z.string() })), + }); + + const response = await axios + .post(`${url.origin}/otto/api/deployment`, payload, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .catch((error) => { + console.error(error.response.data); + throw error; + }); + + return responseSchema.parse(response.data); +} + +export async function getDeploymentStatus({ + url, + token, + deploymentId, +}: { + url: URL; + token: string; + deploymentId: number; +}) { + const schema = z.object({ + response: z.object({ + id: z.number(), + status: z.enum(["queued", "running", "scheduled", "complete", "aborted", "unknown"]), + running: z.coerce.boolean(), + created_at: z.string(), + started_at: z.string(), + updated_at: z.string(), + }), + messages: z.array(z.object({ code: z.number(), text: z.string() })), + }); + + const response = await axios.get(`${url.origin}/otto/api/deployment/${deploymentId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return schema.parse(response.data); +} diff --git a/packages/cli-old/src/cli/prompts.ts b/packages/cli-old/src/cli/prompts.ts new file mode 100644 index 00000000..c4b7900e --- /dev/null +++ b/packages/cli-old/src/cli/prompts.ts @@ -0,0 +1,188 @@ +import * as clack from "@clack/prompts"; +import { + checkbox as inquirerCheckbox, + confirm as inquirerConfirm, + input as inquirerInput, + password as inquirerPassword, + search as inquirerSearch, + select as inquirerSelect, +} from "@inquirer/prompts"; + +const CANCEL_SYMBOL = Symbol.for("@proofkit/cli/prompt-cancelled"); + +export const intro = clack.intro; +export const outro = clack.outro; +export const note = clack.note; +export const log = clack.log; +export const spinner = clack.spinner; +export const cancel = clack.cancel; + +export interface PromptOption { + value: T; + label: string; + hint?: string; + disabled?: boolean | string; +} + +export interface SearchPromptOption extends PromptOption { + keywords?: readonly string[]; +} + +function normalizeValidate( + validate: ((value: string) => string | undefined) | undefined, +): ((value: string) => string | boolean) | undefined { + if (!validate) { + return undefined; + } + + return (value: string) => validate(value) ?? true; +} + +function normalizeDisabledMessage(value: boolean | string | undefined) { + if (typeof value === "string") { + return value; + } + return value ? true : undefined; +} + +function isPromptCancel(error: unknown) { + return error instanceof Error && error.name === "ExitPromptError"; +} + +function withCancelSentinel(fn: () => Promise): Promise { + return fn().catch((error: unknown) => { + if (isPromptCancel(error)) { + return CANCEL_SYMBOL; + } + throw error; + }); +} + +export function isCancel(value: unknown): value is symbol { + return value === CANCEL_SYMBOL || clack.isCancel(value); +} + +function matchesSearch(option: SearchPromptOption, query: string) { + const haystack = [option.label, option.hint ?? "", ...(option.keywords ?? [])].join(" ").toLowerCase(); + return haystack.includes(query.trim().toLowerCase()); +} + +export function filterSearchOptions( + options: readonly SearchPromptOption[], + query: string | undefined, +) { + const term = query?.trim(); + if (!term) { + return options; + } + + return options.filter((option) => matchesSearch(option, term)); +} + +export function text(options: { + message: string; + defaultValue?: string; + placeholder?: string; + validate?: (value: string) => string | undefined; +}) { + return withCancelSentinel(() => + inquirerInput({ + message: options.message, + default: options.defaultValue, + validate: normalizeValidate(options.validate), + }), + ); +} + +export function password(options: { message: string; validate?: (value: string) => string | undefined }) { + return withCancelSentinel(() => + inquirerPassword({ + message: options.message, + validate: normalizeValidate(options.validate), + }), + ); +} + +export function confirm(options: { message: string; initialValue?: boolean; active?: string; inactive?: string }) { + return withCancelSentinel( + () => + inquirerConfirm({ + message: options.message, + default: options.initialValue, + }) as Promise, + ); +} + +export function select(options: { + message: string; + options: PromptOption[]; + maxItems?: number; + initialValue?: T; +}) { + return withCancelSentinel(() => + inquirerSelect({ + message: options.message, + pageSize: options.maxItems ?? 10, + default: options.initialValue, + choices: options.options.map((option) => ({ + value: option.value, + name: option.label, + description: option.hint, + disabled: normalizeDisabledMessage(option.disabled), + })), + }), + ); +} + +export function searchSelect(options: { + message: string; + searchLabel?: string; + emptyMessage?: string; + options: SearchPromptOption[]; +}) { + return withCancelSentinel(() => + inquirerSearch({ + message: options.message, + pageSize: 10, + source: (input) => { + const filtered = filterSearchOptions(options.options, input); + if (filtered.length === 0) { + return [ + { + value: "__no_matches__" as T, + name: options.emptyMessage ?? "No matches found. Keep typing to refine your search.", + disabled: options.emptyMessage ?? "No matches found", + }, + ]; + } + + return filtered.map((option) => ({ + value: option.value, + name: option.label, + description: option.hint, + disabled: normalizeDisabledMessage(option.disabled), + })); + }, + }), + ); +} + +export function multiSearchSelect(options: { + message: string; + options: SearchPromptOption[]; + required?: boolean; +}) { + return withCancelSentinel(() => + inquirerCheckbox({ + message: options.message, + pageSize: 10, + required: options.required, + choices: options.options.map((option) => ({ + value: option.value, + name: option.label, + description: option.hint, + disabled: normalizeDisabledMessage(option.disabled), + })), + }), + ); +} diff --git a/packages/cli-old/src/cli/react-email.ts b/packages/cli-old/src/cli/react-email.ts new file mode 100644 index 00000000..f0ac245a --- /dev/null +++ b/packages/cli-old/src/cli/react-email.ts @@ -0,0 +1,27 @@ +import { Command, Option } from "commander"; +import * as p from "~/cli/prompts.js"; + +import { installReactEmail } from "~/installers/react-email.js"; + +export const runAddReactEmailCommand = async ({ + noInstall, + installServerFiles, +}: { + noInstall?: boolean; + installServerFiles?: boolean; +} = {}) => { + const spinner = p.spinner(); + spinner.start("Adding React Email"); + await installReactEmail({ noInstall, installServerFiles }); + spinner.stop("React Email added"); +}; + +export const makeAddReactEmailCommand = () => { + const addReactEmailCommand = new Command("react-email") + .description("Add React Email scaffolding to your project") + .addOption(new Option("--noInstall", "Do not run your package manager install command").default(false)) + .option("--installServerFiles", "Also scaffold provider-specific server email files", false) + .action((args: { noInstall?: boolean; installServerFiles?: boolean }) => runAddReactEmailCommand(args)); + + return addReactEmailCommand; +}; diff --git a/packages/cli-old/src/cli/remove/data-source.ts b/packages/cli-old/src/cli/remove/data-source.ts new file mode 100644 index 00000000..dbc6aebf --- /dev/null +++ b/packages/cli-old/src/cli/remove/data-source.ts @@ -0,0 +1,153 @@ +import path from "node:path"; +import { Command } from "commander"; +import dotenv from "dotenv"; +import fs from "fs-extra"; +import { z } from "zod/v4"; +import * as p from "~/cli/prompts.js"; + +import { removeFromFmschemaConfig, runCodegenCommand } from "~/generators/fmdapi.js"; +import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; +import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; +import { type DataSource, getSettings, setSettings } from "~/utils/parseSettings.js"; +import { abortIfCancel, ensureProofKitProject, UserAbortedError } from "../utils.js"; + +function getDataSourceInfo(source: DataSource) { + if (source.type !== "fm") { + return source.type; + } + + const envFile = path.join(state.projectDir, ".env"); + if (fs.existsSync(envFile)) { + dotenv.config({ path: envFile }); + } + + const server = process.env[source.envNames.server] || "unknown server"; + const database = process.env[source.envNames.database] || "unknown database"; + + try { + // Format the server URL to be more readable + const serverUrl = new URL(server); + const formattedServer = serverUrl.hostname; + return `${formattedServer}/${database}`; + } catch (error) { + if (state.debug) { + console.error("Error parsing server URL:", error); + } + return `${server}/${database}`; + } +} + +export const runRemoveDataSourceCommand = async (name?: string) => { + const settings = getSettings(); + + if (settings.dataSources.length === 0) { + p.note("No data sources found in your project."); + return; + } + + let dataSourceName = name; + + // If no name provided, prompt for selection + if (dataSourceName) { + // Validate that the provided name exists + const dataSourceExists = settings.dataSources.some((source) => source.name === dataSourceName); + if (!dataSourceExists) { + throw new Error(`Data source "${dataSourceName}" not found in your project.`); + } + } else { + dataSourceName = abortIfCancel( + await p.select({ + message: "Which data source do you want to remove?", + options: settings.dataSources.map((source) => { + let info = ""; + try { + info = getDataSourceInfo(source); + } catch (error) { + if (state.debug) { + console.error("Error getting data source info:", error); + } + info = "unknown connection"; + } + return { + label: `${source.name} (${info})`, + value: source.name, + }; + }), + }), + ); + } + + let confirmed = true; + if (!isNonInteractiveMode()) { + confirmed = abortIfCancel( + await p.confirm({ + message: `Are you sure you want to remove the data source "${dataSourceName}"? This will only remove it from your configuration, not replace any possible usage, which may cause TypeScript errors.`, + }), + ); + + if (!confirmed) { + throw new UserAbortedError(); + } + } + + // Get the data source before removing it + const dataSource = settings.dataSources.find((source) => source.name === dataSourceName); + + // Remove the data source from settings + settings.dataSources = settings.dataSources.filter((source) => source.name !== dataSourceName); + + // Save the updated settings + setSettings(settings); + + if (dataSource?.type === "fm") { + // For FileMaker data sources, remove from fmschema.config.mjs + removeFromFmschemaConfig({ + dataSourceName, + }); + + if (state.debug) { + p.note("Removed schemas from fmschema.config.mjs"); + } + + // Remove the schema folder for this data source + const schemaFolderPath = path.join(state.projectDir, "src", "config", "schemas", dataSourceName); + if (fs.existsSync(schemaFolderPath)) { + fs.removeSync(schemaFolderPath); + if (state.debug) { + p.note(`Removed schema folder at ${schemaFolderPath}`); + } + } + + // Run typegen to regenerate types + await runCodegenCommand(); + if (state.debug) { + p.note("Successfully regenerated types"); + } + } + + p.note(`Successfully removed data source "${dataSourceName}"`); +}; + +export const makeRemoveDataSourceCommand = () => { + const removeDataSourceCommand = new Command("data") + .description("Remove a data source from your project") + .option("--name ", "Name of the data source to remove") + .addOption(ciOption) + .addOption(nonInteractiveOption) + .addOption(debugOption) + .action(async (options) => { + const schema = z.object({ + name: z.string().optional(), + }); + const validated = schema.parse(options); + await runRemoveDataSourceCommand(validated.name); + }); + + removeDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => { + initProgramState(actionCommand.opts()); + state.baseCommand = "remove"; + ensureProofKitProject({ commandName: "remove" }); + }); + + return removeDataSourceCommand; +}; diff --git a/packages/cli-old/src/cli/remove/index.ts b/packages/cli-old/src/cli/remove/index.ts new file mode 100644 index 00000000..954e8765 --- /dev/null +++ b/packages/cli-old/src/cli/remove/index.ts @@ -0,0 +1,72 @@ +import { Command } from "commander"; +import * as p from "~/cli/prompts.js"; + +import { ciOption, debugOption } from "~/globalOptions.js"; +import { initProgramState, state } from "~/state.js"; +import { getSettings } from "~/utils/parseSettings.js"; +import { abortIfCancel, ensureProofKitProject } from "../utils.js"; +import { makeRemoveDataSourceCommand, runRemoveDataSourceCommand } from "./data-source.js"; +import { makeRemovePageCommand, runRemovePageAction } from "./page.js"; +import { makeRemoveSchemaCommand, runRemoveSchemaAction } from "./schema.js"; + +export const runRemove = async (_name: string | undefined) => { + const settings = getSettings(); + + const removeType = abortIfCancel( + await p.select({ + message: "What do you want to remove from your project?", + options: [ + { label: "Page", value: "page" }, + { + label: "Schema", + value: "schema", + hint: "remove a table or layout schema", + }, + ...(settings.appType === "browser" + ? [ + { + label: "Data Source", + value: "data", + hint: "remove a database or FileMaker connection", + }, + ] + : []), + ], + }), + ); + + if (removeType === "data") { + await runRemoveDataSourceCommand(); + } else if (removeType === "page") { + await runRemovePageAction(); + } else if (removeType === "schema") { + await runRemoveSchemaAction(); + } +}; + +export function makeRemoveCommand() { + const removeCommand = new Command("remove") + .description("Remove a component from your project") + .argument("[name]", "Type of component to remove") + .addOption(ciOption) + .addOption(debugOption) + .action(runRemove); + + removeCommand.hook("preAction", (_thisCommand, _actionCommand) => { + initProgramState(_actionCommand.opts()); + state.baseCommand = "remove"; + ensureProofKitProject({ commandName: "remove" }); + }); + removeCommand.hook("preSubcommand", (_thisCommand, _subCommand) => { + initProgramState(_subCommand.opts()); + state.baseCommand = "remove"; + ensureProofKitProject({ commandName: "remove" }); + }); + + // Add subcommands + removeCommand.addCommand(makeRemoveDataSourceCommand()); + removeCommand.addCommand(makeRemovePageCommand()); + removeCommand.addCommand(makeRemoveSchemaCommand()); + + return removeCommand; +} diff --git a/packages/cli-old/src/cli/remove/page.ts b/packages/cli-old/src/cli/remove/page.ts new file mode 100644 index 00000000..d6574589 --- /dev/null +++ b/packages/cli-old/src/cli/remove/page.ts @@ -0,0 +1,214 @@ +import path from "node:path"; +import { Command } from "commander"; +import fs from "fs-extra"; +import { Node, type Project, type PropertyAssignment, SyntaxKind } from "ts-morph"; +import * as p from "~/cli/prompts.js"; + +import { ciOption, debugOption } from "~/globalOptions.js"; +import { initProgramState, state } from "~/state.js"; +import { getSettings } from "~/utils/parseSettings.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; +import { abortIfCancel, ensureProofKitProject } from "../utils.js"; + +const getExistingRoutes = (project: Project): { label: string; href: string }[] => { + const navFilePath = path.join(state.projectDir, "src/app/navigation.tsx"); + + // If navigation file doesn't exist (e.g., webviewer apps), there are no nav routes to remove + if (!fs.existsSync(navFilePath)) { + return []; + } + + const sourceFile = project.addSourceFileAtPath(navFilePath); + + const routes: { label: string; href: string }[] = []; + + // Get primary routes + const primaryRoutes = sourceFile + .getVariableDeclaration("primaryRoutes") + ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) + ?.getElements(); + + if (primaryRoutes) { + for (const element of primaryRoutes) { + if (Node.isObjectLiteralExpression(element)) { + const labelProp = element + .getProperties() + .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "label"); + const hrefProp = element + .getProperties() + .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); + + const label = labelProp?.getInitializer()?.getText().replace(/['"]/g, ""); + const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); + + if (label && href) { + routes.push({ label, href }); + } + } + } + } + + // Get secondary routes + const secondaryRoutes = sourceFile + .getVariableDeclaration("secondaryRoutes") + ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) + ?.getElements(); + + if (secondaryRoutes) { + for (const element of secondaryRoutes) { + if (Node.isObjectLiteralExpression(element)) { + const labelProp = element + .getProperties() + .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "label"); + const hrefProp = element + .getProperties() + .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); + + const label = labelProp?.getInitializer()?.getText().replace(/['"]/g, ""); + const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); + + if (label && href) { + routes.push({ label, href }); + } + } + } + } + + return routes; +}; + +const removeRouteFromNav = async (project: Project, routeToRemove: string) => { + const navFilePath = path.join(state.projectDir, "src/app/navigation.tsx"); + + // Skip if there is no navigation file + if (!fs.existsSync(navFilePath)) { + return; + } + + const sourceFile = project.addSourceFileAtPath(navFilePath); + + // Remove from primary routes + const primaryRoutes = sourceFile + .getVariableDeclaration("primaryRoutes") + ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); + + if (primaryRoutes) { + const elements = primaryRoutes.getElements(); + for (let i = elements.length - 1; i >= 0; i--) { + const element = elements[i]; + if (Node.isObjectLiteralExpression(element)) { + const hrefProp = element + .getProperties() + .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); + + const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); + + if (href === routeToRemove) { + primaryRoutes.removeElement(i); + } + } + } + } + + // Remove from secondary routes + const secondaryRoutes = sourceFile + .getVariableDeclaration("secondaryRoutes") + ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); + + if (secondaryRoutes) { + const elements = secondaryRoutes.getElements(); + for (let i = elements.length - 1; i >= 0; i--) { + const element = elements[i]; + if (Node.isObjectLiteralExpression(element)) { + const hrefProp = element + .getProperties() + .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); + + const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); + + if (href === routeToRemove) { + secondaryRoutes.removeElement(i); + } + } + } + } + + await formatAndSaveSourceFiles(project); +}; + +export const runRemovePageAction = async (routeName?: string) => { + const _settings = getSettings(); + const projectDir = state.projectDir; + const project = getNewProject(projectDir); + + // Get existing routes + const routes = getExistingRoutes(project); + + if (routes.length === 0) { + return p.cancel("No pages found in the navigation."); + } + + let selectedRouteName = routeName; + if (!selectedRouteName) { + selectedRouteName = abortIfCancel( + await p.select({ + message: "Select the page to remove", + options: routes.map((route) => ({ + label: `${route.label} (${route.href})`, + value: route.href, + })), + }), + ); + } + + if (!selectedRouteName.startsWith("/")) { + selectedRouteName = `/${selectedRouteName}`; + } + + const pagePath = + state.appType === "browser" + ? path.join(projectDir, "src/app/(main)", selectedRouteName) + : path.join(projectDir, "src/routes", selectedRouteName); + + const spinner = p.spinner(); + spinner.start("Removing page"); + + try { + // Check if directory exists + if (!fs.existsSync(pagePath)) { + spinner.stop("Page not found!"); + return p.cancel(`Page at ${selectedRouteName} does not exist`); + } + + // Remove from navigation first (if present) + await removeRouteFromNav(project, selectedRouteName); + + // Remove the page directory + await fs.remove(pagePath); + + spinner.stop("Page removed successfully!"); + } catch (error) { + spinner.stop("Failed to remove page!"); + console.error("Error removing page:", error); + process.exit(1); + } +}; + +export const makeRemovePageCommand = () => { + const removePageCommand = new Command("page") + .description("Remove a page from your project") + .argument("[route]", "The route of the page to remove") + .addOption(ciOption) + .addOption(debugOption) + .action(async (route: string) => { + await runRemovePageAction(route); + }); + + removePageCommand.hook("preAction", (_thisCommand, actionCommand) => { + initProgramState(actionCommand.opts()); + state.baseCommand = "remove"; + ensureProofKitProject({ commandName: "remove" }); + }); + + return removePageCommand; +}; diff --git a/packages/cli-old/src/cli/remove/schema.ts b/packages/cli-old/src/cli/remove/schema.ts new file mode 100644 index 00000000..4cc40088 --- /dev/null +++ b/packages/cli-old/src/cli/remove/schema.ts @@ -0,0 +1,100 @@ +import { Command } from "commander"; +import { z } from "zod/v4"; +import * as p from "~/cli/prompts.js"; + +import { getExistingSchemas, removeLayout } from "~/generators/fmdapi.js"; +import { state } from "~/state.js"; +import { getSettings, type Settings } from "~/utils/parseSettings.js"; +import { abortIfCancel } from "../utils.js"; + +export const runRemoveSchemaAction = async (opts?: { + projectDir?: string; + settings?: Settings; + sourceName?: string; + schemaName?: string; +}) => { + const settings = opts?.settings ?? getSettings(); + const projectDir = opts?.projectDir ?? state.projectDir; + let sourceName = opts?.sourceName; + + // If there is more than one fm data source, prompt for which one to remove from + if (!sourceName && settings.dataSources.filter((s) => s.type === "fm").length > 1) { + const dataSourceName = await p.select({ + message: "Which FileMaker data source do you want to remove a layout from?", + options: settings.dataSources.filter((s) => s.type === "fm").map((s) => ({ label: s.name, value: s.name })), + }); + if (p.isCancel(dataSourceName)) { + p.cancel(); + process.exit(0); + } + sourceName = z.string().parse(dataSourceName); + } + + if (!sourceName) { + sourceName = "filemaker"; + } + + const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === sourceName); + if (!dataSource) { + throw new Error(`FileMaker data source ${sourceName} not found in your ProofKit config`); + } + + // Get existing schemas for this data source + const existingSchemas = getExistingSchemas({ + projectDir, + dataSourceName: sourceName, + }); + + if (existingSchemas.length === 0) { + p.note(`No layouts found in data source "${sourceName}"`, "Nothing to remove"); + return; + } + + // Show existing schemas and let user pick one to remove + const schemaToRemove = + opts?.schemaName ?? + abortIfCancel( + await p.select({ + message: "Select a layout to remove", + options: existingSchemas + .map((schema) => ({ + label: `${schema.layout} (${schema.schemaName})`, + value: schema.schemaName ?? "", + })) + .filter((opt) => opt.value !== ""), + }), + ); + + // Confirm removal + const confirmRemoval = await p.confirm({ + message: `Are you sure you want to remove the layout "${schemaToRemove}"?`, + initialValue: false, + }); + + if (p.isCancel(confirmRemoval) || !confirmRemoval) { + p.cancel("Operation cancelled"); + process.exit(0); + } + + // Remove the schema + await removeLayout({ + projectDir, + dataSourceName: sourceName, + schemaName: schemaToRemove, + runCodegen: true, + }); + + p.outro(`Layout "${schemaToRemove}" has been removed from your project`); +}; + +export const makeRemoveSchemaCommand = () => { + const removeSchemaCommand = new Command("layout") + .alias("schema") + .description("Remove a layout from your fmschema file") + .action(async (opts: { settings: Settings }) => { + const settings = opts.settings; + await runRemoveSchemaAction({ settings }); + }); + + return removeSchemaCommand; +}; diff --git a/packages/cli-old/src/cli/tanstack-query.ts b/packages/cli-old/src/cli/tanstack-query.ts new file mode 100644 index 00000000..fb29fac0 --- /dev/null +++ b/packages/cli-old/src/cli/tanstack-query.ts @@ -0,0 +1,19 @@ +import { Command } from "commander"; +import * as p from "~/cli/prompts.js"; + +import { injectTanstackQuery } from "~/generators/tanstack-query.js"; + +export const runAddTanstackQueryCommand = async () => { + const spinner = p.spinner(); + spinner.start("Adding Tanstack Query"); + await injectTanstackQuery(); + spinner.stop("Tanstack Query added"); +}; + +export const makeAddTanstackQueryCommand = () => { + const addTanstackQueryCommand = new Command("tanstack-query") + .description("Add Tanstack Query to your project") + .action(runAddTanstackQueryCommand); + + return addTanstackQueryCommand; +}; diff --git a/packages/cli-old/src/cli/typegen/index.ts b/packages/cli-old/src/cli/typegen/index.ts new file mode 100644 index 00000000..23a4f61c --- /dev/null +++ b/packages/cli-old/src/cli/typegen/index.ts @@ -0,0 +1,20 @@ +import { Command } from "commander"; + +import { runCodegenCommand } from "~/generators/fmdapi.js"; +import type { Settings } from "~/utils/parseSettings.js"; +import { ensureProofKitProject } from "../utils.js"; + +export async function runTypegen(_opts: { settings: Settings }) { + await runCodegenCommand(); +} + +export const makeTypegenCommand = () => { + const typegenCommand = new Command("typegen").description("Generate types for your project").action(runTypegen); + + typegenCommand.hook("preAction", (_thisCommand, actionCommand) => { + const settings = ensureProofKitProject({ commandName: "typegen" }); + actionCommand.setOptionValue("settings", settings); + }); + + return typegenCommand; +}; diff --git a/packages/cli-old/src/cli/update/index.ts b/packages/cli-old/src/cli/update/index.ts new file mode 100644 index 00000000..93eca92b --- /dev/null +++ b/packages/cli-old/src/cli/update/index.ts @@ -0,0 +1,28 @@ +import chalk from "chalk"; +import { Command } from "commander"; + +import { initProgramState, state } from "~/state.js"; +import { runAllAvailableUpgrades } from "~/upgrades/index.js"; +import { logger } from "~/utils/logger.js"; +import { ensureProofKitProject } from "../utils.js"; + +export const runUpgrade = async () => { + initProgramState({}); + state.baseCommand = "upgrade"; + ensureProofKitProject({ commandName: "upgrade" }); + + logger.info("\nUpgrading ProofKit components...\n"); + + try { + await runAllAvailableUpgrades(); + logger.info(chalk.green("✔ Successfully upgraded components\n")); + } catch (error) { + logger.error("Failed to upgrade components:", error); + process.exit(1); + } +}; + +export const upgrade = new Command() + .name("upgrade") + .description("Upgrade ProofKit components in your project") + .action(runUpgrade); diff --git a/packages/cli-old/src/cli/update/makeUpgradeCommand.ts b/packages/cli-old/src/cli/update/makeUpgradeCommand.ts new file mode 100644 index 00000000..8232b3fe --- /dev/null +++ b/packages/cli-old/src/cli/update/makeUpgradeCommand.ts @@ -0,0 +1,25 @@ +import { Command } from "commander"; + +import { ciOption } from "~/globalOptions.js"; +import { initProgramState, state } from "~/state.js"; +import { ensureProofKitProject } from "../utils.js"; +import { runUpgrade } from "./index.js"; + +export const makeUpgradeCommand = () => { + const upgradeCommand = new Command("upgrade") + .description("Upgrade ProofKit components in your project") + .addOption(ciOption) + .action(async (args) => { + initProgramState(args); + + await runUpgrade(); + }); + + upgradeCommand.hook("preAction", (_thisCommand, _actionCommand) => { + initProgramState(_actionCommand.opts()); + state.baseCommand = "upgrade"; + ensureProofKitProject({ commandName: "upgrade" }); + }); + + return upgradeCommand; +}; diff --git a/packages/cli-old/src/cli/utils.ts b/packages/cli-old/src/cli/utils.ts new file mode 100644 index 00000000..37a6897f --- /dev/null +++ b/packages/cli-old/src/cli/utils.ts @@ -0,0 +1,49 @@ +import path from "node:path"; +import chalk from "chalk"; +import fs from "fs-extra"; +import z, { ZodError } from "zod/v4"; + +import { cancel, isCancel } from "~/cli/prompts.js"; +import { npmName } from "~/consts.js"; +import { getSettings } from "~/utils/parseSettings.js"; + +/** + * Runs before any add command is run. Checks if the user is in a ProofKit project and if the + * proofkit.json file is valid. + */ +export const ensureProofKitProject = ({ commandName }: { commandName: string }) => { + const settingsExists = fs.existsSync(path.join(process.cwd(), "proofkit.json")); + if (!settingsExists) { + console.log( + chalk.yellow( + `The "${commandName}" command requires an existing ProofKit project. +Please run " ${npmName} init" first, or try this command again when inside a ProofKit project.`, + ), + ); + process.exit(1); + } + + try { + return getSettings(); + } catch (error) { + console.log(chalk.red("Error parsing ProofKit settings file:")); + if (error instanceof ZodError) { + console.log(z.prettifyError(error)); + } else { + console.log(error); + } + + process.exit(1); + } +}; + +export class UserAbortedError extends Error {} +export function abortIfCancel(value: symbol | string): string; +export function abortIfCancel(value: symbol | T): T; +export function abortIfCancel(value: T | symbol): T { + if (isCancel(value)) { + cancel(); + throw new UserAbortedError(); + } + return value; +} diff --git a/packages/cli-old/src/consts.ts b/packages/cli-old/src/consts.ts new file mode 100644 index 00000000..d9c413e0 --- /dev/null +++ b/packages/cli-old/src/consts.ts @@ -0,0 +1,35 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { getVersion } from "./utils/getProofKitVersion.js"; + +// Path is in relation to a single index.js file inside ./dist +const __filename = fileURLToPath(import.meta.url); +const distPath = path.dirname(__filename); +export const PKG_ROOT = path.join(distPath, "../"); +export const cliName = "proofkit"; +export const npmName = "@proofkit/cli"; +export const DOCS_URL = "https://proofkit.dev"; + +const version = getVersion(); +const versionCharLength = version.length; +//export const PKG_ROOT = path.dirname(require.main.filename); + +export const TITLE_TEXT = ` + _______ ___ ___ ____ _ _ +|_ __ \\ .' ..]|_ ||_ _| (_) / |_ + | |__) |_ .--. .--. .--. _| |_ | |_/ / __ \`| |-' + | ___/[ \`/'\`\\]/ .'\`\\ \\/ .'\`\\ \\'-| |-' | __'. [ | | | + _| |_ | | | \\__. || \\__. | | | _| | \\ \\_ | | | |, +|_____| [___] '.__.' '.__.' [___] |____||____|[___]\\__/ +${" ".repeat(61 - versionCharLength)}v${version} +`; +export const DEFAULT_APP_NAME = "my-proofkit-app"; +export const CREATE_FM_APP = cliName; + +// Registry URL is injected at build time via tsdown define +declare const __REGISTRY_URL__: string; +// Provide a safe fallback when running from source (not built) +export const DEFAULT_REGISTRY_URL = + // typeof check avoids ReferenceError if not defined at runtime + typeof __REGISTRY_URL__ !== "undefined" && __REGISTRY_URL__ ? __REGISTRY_URL__ : "https://proofkit.dev"; diff --git a/packages/cli-old/src/generators/auth.ts b/packages/cli-old/src/generators/auth.ts new file mode 100644 index 00000000..3ecd4080 --- /dev/null +++ b/packages/cli-old/src/generators/auth.ts @@ -0,0 +1,83 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { glob } from "glob"; + +import { installDependencies } from "~/helpers/installDependencies.js"; +import { betterAuthInstaller } from "~/installers/better-auth.js"; +import { clerkInstaller } from "~/installers/clerk.js"; +import { proofkitAuthInstaller } from "~/installers/proofkit-auth.js"; +import { state } from "~/state.js"; +import { getSettings, mergeSettings } from "~/utils/parseSettings.js"; + +export async function addAuth({ + options, + noInstall = false, + projectDir = process.cwd(), +}: { + options: + | { type: "clerk" } + | { + type: "fmaddon"; + emailProvider?: "plunk" | "resend"; + apiKey?: string; + } + | { type: "better-auth" }; + projectDir?: string; + noInstall?: boolean; +}) { + const settings = getSettings(); + if (settings.ui === "shadcn") { + throw new Error("Shadcn projects should add auth using the template registry"); + } + if (settings.auth.type !== "none") { + throw new Error("Auth already exists"); + } + if (!settings.dataSources.some((o) => o.type === "fm") && options.type === "fmaddon") { + throw new Error("A FileMaker data source is required to use the FM Add-on Auth"); + } + if (!settings.dataSources.some((o) => o.type === "fm") && options.type === "better-auth") { + throw new Error("A FileMaker data source is required to use the Better-Auth"); + } + + if (options.type === "clerk") { + await addClerkAuth({ projectDir }); + } else if (options.type === "fmaddon") { + await addFmaddonAuth(); + } + + // Replace actionClient with authedActionClient in all action files + await replaceActionClientWithAuthed(); + + if (!noInstall) { + await installDependencies({ projectDir }); + } +} + +async function addClerkAuth({ projectDir = process.cwd() }: { projectDir?: string }) { + await clerkInstaller({ projectDir }); + mergeSettings({ auth: { type: "clerk" } }); +} + +async function addFmaddonAuth() { + await proofkitAuthInstaller(); + mergeSettings({ auth: { type: "fmaddon" } }); +} + +async function replaceActionClientWithAuthed() { + const projectDir = state.projectDir; + const actionFiles = await glob("src/app/(main)/**/actions.ts", { + cwd: projectDir, + }); + + for (const file of actionFiles) { + const fullPath = path.join(projectDir, file); + const content = readFileSync(fullPath, "utf-8"); + const updatedContent = content.replace(/actionClient/g, "authedActionClient"); + writeFileSync(fullPath, updatedContent); + } +} + +async function _addBetterAuth() { + await betterAuthInstaller(); + mergeSettings({ auth: { type: "better-auth" } }); +} diff --git a/packages/cli-old/src/generators/fmdapi.ts b/packages/cli-old/src/generators/fmdapi.ts new file mode 100644 index 00000000..a7c16ea9 --- /dev/null +++ b/packages/cli-old/src/generators/fmdapi.ts @@ -0,0 +1,525 @@ +import path from "node:path"; +import { generateTypedClients } from "@proofkit/typegen"; +import type { typegenConfigSingle } from "@proofkit/typegen/config"; +import { config as dotenvConfig } from "dotenv"; +import fs from "fs-extra"; +import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser"; +import { SyntaxKind } from "ts-morph"; +import type { z } from "zod/v4"; + +import { state } from "~/state.js"; +import { logger } from "~/utils/logger.js"; +import type { envNamesSchema } from "~/utils/parseSettings.js"; +import { getNewProject } from "~/utils/ts-morph.js"; + +// Input schema for functions like addLayout +// This might be different from the layout config stored in the file +interface Schema { + layoutName: string; + schemaName: string; + valueLists?: "strict" | "allowEmpty" | "ignore"; + generateClient?: boolean; + strictNumbers?: boolean; +} + +// For any data source configuration object (fmdapi or fmodata) +type AnyDataSourceConfig = z.infer; +// For a single fmdapi data source configuration object +type FmdapiDataSourceConfig = Extract; +// For a single layout configuration object within a data source +type ImportedLayoutConfig = FmdapiDataSourceConfig["layouts"][number]; + +// This type represents the actual structure of the JSONC file, including $schema +interface FullProofkitTypegenJsonFile { + $schema?: string; + config: AnyDataSourceConfig | AnyDataSourceConfig[]; +} + +const typegenConfigFileName = "proofkit-typegen.config.jsonc"; + +// Helper function to normalize data sources by adding default type for backwards compatibility +// This mirrors the zod preprocess in @proofkit/typegen that defaults type to "fmdapi" +function normalizeDataSource(ds: AnyDataSourceConfig): AnyDataSourceConfig { + if (!("type" in ds) || ds.type === undefined) { + return { ...(ds as object), type: "fmdapi" } as AnyDataSourceConfig; + } + return ds; +} + +function normalizeConfig( + config: AnyDataSourceConfig | AnyDataSourceConfig[], +): AnyDataSourceConfig | AnyDataSourceConfig[] { + if (Array.isArray(config)) { + return config.map(normalizeDataSource); + } + return normalizeDataSource(config); +} + +// Helper functions for JSON config +async function readJsonConfigFile(configPath: string): Promise { + if (!fs.existsSync(configPath)) { + return null; + } + try { + const fileContent = await fs.readFile(configPath, "utf8"); + const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; + // Normalize config to add default type for backwards compatibility + if (parsed.config) { + parsed.config = normalizeConfig(parsed.config); + } + return parsed; + } catch (error) { + console.error(`Error reading or parsing JSONC config at ${configPath}:`, error); + // Return a default structure for the *file* if parsing fails but file exists + return { + $schema: "https://proofkit.dev/typegen-config-schema.json", + config: [], + }; + } +} + +async function writeJsonConfigFile(configPath: string, fileContent: FullProofkitTypegenJsonFile) { + // Check if file exists to preserve comments + if (fs.existsSync(configPath)) { + const originalText = await fs.readFile(configPath, "utf8"); + // Use jsonc-parser's modify function to preserve comments + const edits = modify(originalText, ["config"], fileContent.config, { + formattingOptions: { + tabSize: 2, + insertSpaces: true, + eol: "\n", + }, + }); + const modifiedText = applyEdits(originalText, edits); + await fs.writeFile(configPath, modifiedText, "utf8"); + } else { + // If file doesn't exist, create it with proper formatting + await fs.writeJson(configPath, fileContent, { spaces: 2 }); + } +} + +export async function addLayout({ + projectDir = process.cwd(), + schemas, + runCodegen = true, + dataSourceName, +}: { + projectDir?: string; + schemas: Schema[]; + runCodegen?: boolean; + dataSourceName: string; +}) { + const jsonConfigPath = path.join(projectDir, typegenConfigFileName); + let fileContent = await readJsonConfigFile(jsonConfigPath); + + if (!fileContent) { + fileContent = { + $schema: "https://proofkit.dev/typegen-config-schema.json", + config: [], + }; + } + + // Work with the 'config' property which is TypegenConfig['config'] + const configProperty = fileContent.config; + + let configArray: AnyDataSourceConfig[]; + if (Array.isArray(configProperty)) { + configArray = configProperty; + } else { + configArray = [configProperty]; + fileContent.config = configArray; // Update fileContent to ensure it's an array for later ops + } + + const layoutsToAdd: ImportedLayoutConfig[] = schemas.map((schema) => ({ + layoutName: schema.layoutName, + schemaName: schema.schemaName, + valueLists: schema.valueLists, + generateClient: schema.generateClient, + strictNumbers: schema.strictNumbers, + })); + + let targetDataSource: FmdapiDataSourceConfig | undefined = configArray.find( + (ds): ds is FmdapiDataSourceConfig => + ds.type === "fmdapi" && + (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), + ); + + if (targetDataSource) { + targetDataSource.layouts = targetDataSource.layouts || []; + } else { + targetDataSource = { + type: "fmdapi", + layouts: [], + path: `./src/config/schemas/${dataSourceName}`, + // other default properties for a new DataSourceConfig can be added here if needed + envNames: undefined, + }; + configArray.push(targetDataSource); + } + + targetDataSource.layouts.push(...layoutsToAdd); + // fileContent.config is already pointing to configArray if it was modified + + await writeJsonConfigFile(jsonConfigPath, fileContent); + + if (runCodegen) { + await runCodegenCommand(); + } +} + +export async function addConfig({ + config, + projectDir, + runCodegen = true, +}: { + config: FmdapiDataSourceConfig | FmdapiDataSourceConfig[]; + projectDir: string; + runCodegen?: boolean; +}) { + const jsonConfigPath = path.join(projectDir, typegenConfigFileName); + let fileContent = await readJsonConfigFile(jsonConfigPath); + + const configsToAdd = Array.isArray(config) ? config : [config]; + + if (fileContent) { + if (Array.isArray(fileContent.config)) { + fileContent.config.push(...configsToAdd); + } else { + fileContent.config = [fileContent.config, ...configsToAdd]; + } + } else { + fileContent = { + $schema: "https://proofkit.dev/typegen-config-schema.json", + config: configsToAdd, + }; + } + + await writeJsonConfigFile(jsonConfigPath, fileContent); + + if (runCodegen) { + await runCodegenCommand(); + } +} + +export async function ensureWebviewerFmHttpConfig({ + projectDir, + connectedFileName, + dataSourceName = "filemaker", + baseUrl, +}: { + projectDir: string; + connectedFileName?: string; + dataSourceName?: string; + baseUrl?: string; +}) { + const newConfig: FmdapiDataSourceConfig = { + type: "fmdapi", + path: `./src/config/schemas/${dataSourceName}`, + clearOldFiles: true, + clientSuffix: "Layout", + webviewerScriptName: "ExecuteDataApi", + envNames: undefined, + layouts: [], + fmHttp: { + enabled: true, + ...(baseUrl ? { baseUrl } : {}), + ...(connectedFileName ? { connectedFileName } : {}), + }, + }; + + const jsonConfigPath = path.join(projectDir, typegenConfigFileName); + let fileContent = await readJsonConfigFile(jsonConfigPath); + + if (!fileContent) { + fileContent = { + $schema: "https://proofkit.dev/typegen-config-schema.json", + config: [newConfig], + }; + await writeJsonConfigFile(jsonConfigPath, fileContent); + return; + } + + const configArray = Array.isArray(fileContent.config) ? fileContent.config : [fileContent.config]; + if (!Array.isArray(fileContent.config)) { + fileContent.config = configArray; + } + + const existingConfigIndex = configArray.findIndex( + (config): config is FmdapiDataSourceConfig => config.type === "fmdapi" && config.path === newConfig.path, + ); + + if (existingConfigIndex === -1) { + configArray.push(newConfig); + } else { + const existingConfig = configArray[existingConfigIndex] as FmdapiDataSourceConfig; + configArray[existingConfigIndex] = { + ...existingConfig, + ...newConfig, + layouts: existingConfig.layouts ?? [], + fmHttp: { + enabled: true, + ...(existingConfig.fmHttp ?? {}), + ...(newConfig.fmHttp ?? {}), + }, + }; + } + + await writeJsonConfigFile(jsonConfigPath, fileContent); +} + +export async function runCodegenCommand() { + const projectDir = state.projectDir; + const config = await readJsonConfigFile(path.join(projectDir, typegenConfigFileName)); + if (!config) { + logger.info("no typegen config found, skipping typegen"); + return; + } + + // make sure to load the .env file + dotenvConfig({ path: path.join(projectDir, ".env") }); + await generateTypedClients(config.config, { cwd: projectDir }); +} + +export function getClientSuffix({ + projectDir = process.cwd(), + dataSourceName, +}: { + projectDir?: string; + dataSourceName: string; +}): string { + const jsonConfigPath = path.join(projectDir, typegenConfigFileName); + if (!fs.existsSync(jsonConfigPath)) { + return "Client"; + } + try { + const fileContent = fs.readFileSync(jsonConfigPath, "utf8"); + const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; + + // Normalize config to add default type for backwards compatibility + const normalizedConfig = normalizeConfig(parsed.config); + const configToSearch = Array.isArray(normalizedConfig) ? normalizedConfig : [normalizedConfig]; + + const targetDataSource = configToSearch.find( + (ds): ds is FmdapiDataSourceConfig => + ds.type === "fmdapi" && + (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), + ); + return targetDataSource?.clientSuffix ?? "Client"; + } catch (error) { + console.error(`Error reading or parsing JSONC config for getClientSuffix: ${jsonConfigPath}`, error); + return "Client"; + } +} + +export function getExistingSchemas({ + projectDir = process.cwd(), + dataSourceName, +}: { + projectDir?: string; + dataSourceName: string; +}): { layout?: string; schemaName?: string }[] { + const jsonConfigPath = path.join(projectDir, typegenConfigFileName); + if (!fs.existsSync(jsonConfigPath)) { + return []; + } + try { + const fileContent = fs.readFileSync(jsonConfigPath, "utf8"); + const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; + + // Normalize config to add default type for backwards compatibility + const normalizedConfig = normalizeConfig(parsed.config); + const configToSearch = Array.isArray(normalizedConfig) ? normalizedConfig : [normalizedConfig]; + + const targetDataSource = configToSearch.find( + (ds): ds is FmdapiDataSourceConfig => + ds.type === "fmdapi" && + (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), + ); + + if (targetDataSource?.layouts) { + return targetDataSource.layouts.map((layout) => ({ + layout: layout.layoutName, + schemaName: layout.schemaName, + })); + } + return []; + } catch (error) { + console.error(`Error reading or parsing JSONC config for getExistingSchemas: ${jsonConfigPath}`, error); + return []; + } +} + +export async function addToFmschemaConfig({ + dataSourceName, + envNames, +}: { + dataSourceName: string; + envNames?: z.infer; +}) { + const projectDir = state.projectDir; + const jsonConfigPath = path.join(projectDir, typegenConfigFileName); + let fileContent = await readJsonConfigFile(jsonConfigPath); + + const newDataSource: FmdapiDataSourceConfig = { + type: "fmdapi", + layouts: [], + path: `./src/config/schemas/${dataSourceName}`, + envNames: undefined, + clearOldFiles: true, + clientSuffix: "Layout", + }; + + if (envNames) { + newDataSource.envNames = { + server: envNames.server, + db: envNames.database, + auth: { apiKey: envNames.apiKey }, + }; + } + if (state.appType === "webviewer") { + newDataSource.webviewerScriptName = "ExecuteDataApi"; + } + + if (fileContent) { + let configArray: AnyDataSourceConfig[]; + if (Array.isArray(fileContent.config)) { + configArray = fileContent.config; + } else { + configArray = [fileContent.config]; + fileContent.config = configArray; + } + + const existingDsIndex = configArray.findIndex((ds) => ds.type === "fmdapi" && ds.path === newDataSource.path); + if (existingDsIndex === -1) { + configArray.push(newDataSource); + } else { + const existingConfig = configArray[existingDsIndex] as FmdapiDataSourceConfig; + configArray[existingDsIndex] = { + ...existingConfig, + ...newDataSource, + layouts: newDataSource.layouts.length > 0 ? newDataSource.layouts : existingConfig.layouts || [], + }; + } + } else { + fileContent = { + $schema: "https://proofkit.dev/typegen-config-schema.json", + config: [newDataSource], + }; + } + await writeJsonConfigFile(jsonConfigPath, fileContent); +} + +export function getFieldNamesForSchema({ schemaName, dataSourceName }: { schemaName: string; dataSourceName: string }) { + const projectDir = state.projectDir; + const project = getNewProject(projectDir); + const sourceFilePath = path.join(projectDir, `src/config/schemas/${dataSourceName}/generated/${schemaName}.ts`); + + const sourceFilePathAlternative = path.join(projectDir, `src/config/schemas/${dataSourceName}/${schemaName}.ts`); + + let fileToUse = sourceFilePath; + if (!fs.existsSync(sourceFilePath)) { + if (fs.existsSync(sourceFilePathAlternative)) { + fileToUse = sourceFilePathAlternative; + } else { + return []; + } + } + const sourceFile = project.addSourceFileAtPath(fileToUse); + + const zodSchema = sourceFile.getVariableDeclaration(`Z${schemaName}`); + if (zodSchema) { + const properties = zodSchema + .getInitializer() + ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression) + ?.getProperties(); + return ( + properties?.map((pr) => pr.asKind(SyntaxKind.PropertyAssignment)?.getName()?.replace(/"/g, "")).filter(Boolean) ?? + [] + ); + } + const typeAlias = sourceFile.getTypeAlias(`T${schemaName}`); + const properties = typeAlias?.getFirstDescendantByKind(SyntaxKind.TypeLiteral)?.getProperties(); + return ( + properties?.map((pr) => pr.asKind(SyntaxKind.PropertySignature)?.getName()?.replace(/"/g, "")).filter(Boolean) ?? [] + ); +} + +export async function removeFromFmschemaConfig({ dataSourceName }: { dataSourceName: string }) { + const projectDir = state.projectDir; + const jsonConfigPath = path.join(projectDir, typegenConfigFileName); + const fileContent = await readJsonConfigFile(jsonConfigPath); + + if (!fileContent) { + return; + } + + const pathToRemove = `./src/config/schemas/${dataSourceName}`; + + if (Array.isArray(fileContent.config)) { + fileContent.config = fileContent.config.filter((ds) => !(ds.type === "fmdapi" && ds.path === pathToRemove)); + } else { + const currentConfig = fileContent.config; + if (currentConfig.type === "fmdapi" && currentConfig.path === pathToRemove) { + fileContent.config = []; + } + } + await writeJsonConfigFile(jsonConfigPath, fileContent); +} + +export async function removeLayout({ + projectDir = state.projectDir, + schemaName, + dataSourceName, + runCodegen = true, +}: { + projectDir?: string; + schemaName: string; + dataSourceName: string; + runCodegen?: boolean; +}) { + const jsonConfigPath = path.join(projectDir, typegenConfigFileName); + const fileContent = await readJsonConfigFile(jsonConfigPath); + + if (!fileContent) { + throw new Error(`${typegenConfigFileName} not found, cannot remove layout.`); + } + + let dataSourceModified = false; + const targetDsPath = `./src/config/schemas/${dataSourceName}`; + + let configArray: AnyDataSourceConfig[]; + if (Array.isArray(fileContent.config)) { + configArray = fileContent.config; + } else { + configArray = [fileContent.config]; + fileContent.config = configArray; + } + + const targetDataSource = configArray.find( + (ds): ds is FmdapiDataSourceConfig => ds.type === "fmdapi" && ds.path === targetDsPath, + ); + + if (targetDataSource?.layouts) { + const initialCount = targetDataSource.layouts.length; + targetDataSource.layouts = targetDataSource.layouts.filter((layout) => layout.schemaName !== schemaName); + if (targetDataSource.layouts.length < initialCount) { + dataSourceModified = true; + } + } + + if (dataSourceModified) { + await writeJsonConfigFile(jsonConfigPath, fileContent); + } + + const schemaFilePath = path.join(projectDir, "src", "config", "schemas", dataSourceName, `${schemaName}.ts`); + if (fs.existsSync(schemaFilePath)) { + fs.removeSync(schemaFilePath); + } + + if (runCodegen && dataSourceModified) { + await runCodegenCommand(); + } +} + +// Make sure to remove unused imports like Project, SyntaxKind, etc. if they are no longer used anywhere. +// Also remove getNewProject and formatAndSaveSourceFiles from imports if they were only for config. diff --git a/packages/cli-old/src/generators/route.ts b/packages/cli-old/src/generators/route.ts new file mode 100644 index 00000000..512c9a49 --- /dev/null +++ b/packages/cli-old/src/generators/route.ts @@ -0,0 +1,40 @@ +import path from "node:path"; +import fs from "fs-extra"; +import type { RouteLink } from "index.js"; +import { SyntaxKind } from "ts-morph"; + +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; + +export async function addRouteToNav({ + projectDir, + navType, + ...route +}: Omit & { + projectDir: string; + navType: "primary" | "secondary"; +}) { + const navFilePath = path.join(projectDir, "src/app/navigation.tsx"); + + // If the navigation file doesn't exist (e.g., WebViewer apps), skip adding to nav + if (!fs.existsSync(navFilePath)) { + return; + } + + const project = getNewProject(projectDir); + const sourceFile = project.addSourceFileAtPath(navFilePath); + sourceFile + .getVariableDeclaration(navType === "primary" ? "primaryRoutes" : "secondaryRoutes") + ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) + ?.addElement((writer) => + writer + .block(() => { + writer.write(` + label: "${route.label}", + type: "link", + href: "${route.href}",`); + }) + .write(","), + ); + + await formatAndSaveSourceFiles(project); +} diff --git a/packages/cli-old/src/generators/tanstack-query.ts b/packages/cli-old/src/generators/tanstack-query.ts new file mode 100644 index 00000000..874eee0d --- /dev/null +++ b/packages/cli-old/src/generators/tanstack-query.ts @@ -0,0 +1,97 @@ +import path from "node:path"; +import fs from "fs-extra"; +import { type Project, SyntaxKind } from "ts-morph"; + +import { PKG_ROOT } from "~/consts.js"; +import { state } from "~/state.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { getSettings, setSettings } from "~/utils/parseSettings.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; + +export async function injectTanstackQuery(args?: { project?: Project }) { + const projectDir = state.projectDir; + const settings = getSettings(); + if (settings.ui === "shadcn") { + return false; + } + if (settings.tanstackQuery) { + return false; + } + + addPackageDependency({ + projectDir, + dependencies: ["@tanstack/react-query"], + devMode: false, + }); + addPackageDependency({ + projectDir, + dependencies: ["@tanstack/react-query-devtools"], + devMode: true, + }); + const extrasDir = path.join(PKG_ROOT, "template", "extras"); + + if (state.appType === "browser") { + fs.copySync( + path.join(extrasDir, "config", "get-query-client.ts"), + path.join(projectDir, "src/config/get-query-client.ts"), + ); + fs.copySync( + path.join(extrasDir, "config", "query-provider.tsx"), + path.join(projectDir, "src/config/query-provider.tsx"), + ); + } else if (state.appType === "webviewer") { + fs.copySync( + path.join(extrasDir, "config", "query-provider-vite.tsx"), + path.join(projectDir, "src/config/query-provider.tsx"), + ); + } + + // inject query provider into the root layout + const project = args?.project ?? getNewProject(projectDir); + const rootLayout = project.addSourceFileAtPath( + path.join(projectDir, state.appType === "browser" ? "src/app/layout.tsx" : "src/main.tsx"), + ); + rootLayout.addImportDeclaration({ + moduleSpecifier: "@/config/query-provider", + defaultImport: "QueryProvider", + }); + + if (state.appType === "browser") { + const exportDefault = rootLayout.getFunction((dec) => dec.isDefaultExport()); + const bodyElement = exportDefault + ?.getBody() + ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) + ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) + .find((openingElement) => openingElement.getTagNameNode().getText() === "body") + ?.getParentIfKind(SyntaxKind.JsxElement); + + const childrenText = bodyElement + ?.getJsxChildren() + .map((child) => child.getText()) + .filter(Boolean) + .join("\n"); + + bodyElement?.getChildSyntaxList()?.replaceWithText( + ` + ${childrenText} + `, + ); + } else if (state.appType === "webviewer") { + const mantineProvider = rootLayout + .getDescendantsOfKind(SyntaxKind.JsxElement) + .find((element) => element.getOpeningElement().getTagNameNode().getText() === "MantineProvider"); + + mantineProvider?.replaceWithText( + ` + ${mantineProvider.getText()} + `, + ); + } + + if (!args?.project) { + await formatAndSaveSourceFiles(project); + } + + setSettings({ ...settings, tanstackQuery: true }); + return true; +} diff --git a/packages/cli-old/src/globalOptions.ts b/packages/cli-old/src/globalOptions.ts new file mode 100644 index 00000000..5fbdeef7 --- /dev/null +++ b/packages/cli-old/src/globalOptions.ts @@ -0,0 +1,8 @@ +import { Option } from "commander"; + +export const ciOption = new Option("--ci", "Deprecated alias for --non-interactive").default(false); +export const nonInteractiveOption = new Option( + "--non-interactive", + "Never prompt for input; fail with a clear error when required values are missing", +).default(false); +export const debugOption = new Option("--debug", "Run in debug mode").default(false); diff --git a/packages/cli-old/src/globals.d.ts b/packages/cli-old/src/globals.d.ts new file mode 100644 index 00000000..edd6438c --- /dev/null +++ b/packages/cli-old/src/globals.d.ts @@ -0,0 +1,4 @@ +declare const __FMDAPI_VERSION__: string; +declare const __BETTER_AUTH_VERSION__: string; +declare const __WEBVIEWER_VERSION__: string; +declare const __TYPEGEN_VERSION__: string; diff --git a/packages/cli-old/src/helpers/createProject.ts b/packages/cli-old/src/helpers/createProject.ts new file mode 100644 index 00000000..cb354d95 --- /dev/null +++ b/packages/cli-old/src/helpers/createProject.ts @@ -0,0 +1,129 @@ +import path from "node:path"; + +import { installPackages } from "~/helpers/installPackages.js"; +import { scaffoldProject } from "~/helpers/scaffoldProject.js"; +import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js"; +import type { PkgInstallerMap } from "~/installers/index.js"; +import { state } from "~/state.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; +import { replaceTextInFiles } from "./replaceText.js"; + +interface CreateProjectOptions { + projectName: string; + packages: PkgInstallerMap; + scopedAppName: string; + noInstall: boolean; + force: boolean; + appRouter: boolean; +} + +export const createBareProject = async ({ + projectName, + scopedAppName, + packages, + noInstall, + force, +}: CreateProjectOptions) => { + const pkgManager = getUserPkgManager(); + state.projectDir = path.resolve(process.cwd(), projectName); + + // Bootstraps the base Next.js application + await scaffoldProject({ + projectName, + pkgManager, + scopedAppName, + noInstall, + force, + }); + + addPackageDependency({ + dependencies: ["@proofkit/cli", "@types/node"], + devMode: true, + }); + + // Add new base dependencies for Tailwind v4 and shadcn/ui or legacy Mantine + // These should match the plan and dependencyVersionMap + const NEXT_SHADCN_BASE_DEPS = [ + "@radix-ui/react-slot", + "@tailwindcss/postcss", + "class-variance-authority", + "clsx", + "lucide-react", + "tailwind-merge", + "tailwindcss", + "tw-animate-css", + "next-themes", + ] as AvailableDependencies[]; + const VITE_SHADCN_BASE_DEPS = [ + "@radix-ui/react-slot", + "@tailwindcss/vite", + "@proofkit/fmdapi", + "@proofkit/webviewer", + "class-variance-authority", + "clsx", + "lucide-react", + "tailwind-merge", + "tailwindcss", + "tw-animate-css", + "zod", + ] as AvailableDependencies[]; + const SHADCN_BASE_DEV_DEPS = [] as AvailableDependencies[]; + const VITE_SHADCN_BASE_DEV_DEPS = ["@proofkit/typegen"] as AvailableDependencies[]; + + const MANTINE_DEPS = [ + "@mantine/core", + "@mantine/dates", + "@mantine/hooks", + "@mantine/modals", + "@mantine/notifications", + "mantine-react-table", + ] as AvailableDependencies[]; + const MANTINE_DEV_DEPS = ["postcss", "postcss-preset-mantine", "postcss-simple-vars"] as AvailableDependencies[]; + + if (state.ui === "mantine") { + addPackageDependency({ + dependencies: MANTINE_DEPS, + devMode: false, + }); + addPackageDependency({ + dependencies: MANTINE_DEV_DEPS, + devMode: true, + }); + } else if (state.ui === "shadcn") { + addPackageDependency({ + dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEPS : NEXT_SHADCN_BASE_DEPS, + devMode: false, + }); + addPackageDependency({ + dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEV_DEPS : SHADCN_BASE_DEV_DEPS, + devMode: true, + }); + } else { + throw new Error(`Unsupported UI library: ${state.ui}`); + } + + // Install the selected packages + installPackages({ + projectName, + scopedAppName, + pkgManager, + packages, + noInstall, + }); + + let pkgManagerCommand: string; + if (pkgManager === "pnpm") { + pkgManagerCommand = "pnpm"; + } else if (pkgManager === "bun") { + pkgManagerCommand = "bun"; + } else if (pkgManager === "yarn") { + pkgManagerCommand = "yarn"; + } else { + pkgManagerCommand = "npm run"; + } + + replaceTextInFiles(state.projectDir, "__PNPM_COMMAND__", pkgManagerCommand); + + return state.projectDir; +}; diff --git a/packages/cli-old/src/helpers/fmHttp.ts b/packages/cli-old/src/helpers/fmHttp.ts new file mode 100644 index 00000000..89799b75 --- /dev/null +++ b/packages/cli-old/src/helpers/fmHttp.ts @@ -0,0 +1,56 @@ +const defaultBaseUrl = process.env.FM_HTTP_BASE_URL ?? "http://127.0.0.1:1365"; +const REQUEST_TIMEOUT_MS = 3000; + +export interface FmHttpStatus { + baseUrl: string; + healthy: boolean; + connectedFiles: string[]; +} + +async function fetchWithTimeout(url: string): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + + try { + return await fetch(url, { signal: controller.signal }); + } catch { + return null; + } finally { + clearTimeout(timeoutId); + } +} + +async function readJson(url: string): Promise { + const response = await fetchWithTimeout(url); + + if (!response?.ok) { + return null; + } + + return await response.json().catch(() => null); +} + +export async function getFmHttpStatus(baseUrl = defaultBaseUrl): Promise { + const healthResponse = await fetchWithTimeout(`${baseUrl}/health`); + + if (!healthResponse?.ok) { + return { + baseUrl, + healthy: false, + connectedFiles: [], + }; + } + + const connectedFiles = await readJson(`${baseUrl}/connectedFiles`); + + return { + baseUrl, + healthy: true, + connectedFiles: Array.isArray(connectedFiles) ? connectedFiles : [], + }; +} + +export async function detectConnectedFmFile(baseUrl = defaultBaseUrl): Promise { + const status = await getFmHttpStatus(baseUrl); + return status.connectedFiles[0]; +} diff --git a/packages/cli-old/src/helpers/git.ts b/packages/cli-old/src/helpers/git.ts new file mode 100644 index 00000000..bdeaefee --- /dev/null +++ b/packages/cli-old/src/helpers/git.ts @@ -0,0 +1,140 @@ +import { execSync } from "node:child_process"; +import path from "node:path"; +import chalk from "chalk"; +import { execa } from "execa"; +import fs from "fs-extra"; +import ora from "ora"; +import * as p from "~/cli/prompts.js"; + +import { isNonInteractiveMode } from "~/state.js"; +import { logger } from "~/utils/logger.js"; + +const isGitInstalled = (dir: string): boolean => { + try { + execSync("git --version", { cwd: dir }); + return true; + } catch (_e) { + return false; + } +}; + +/** @returns Whether or not the provided directory has a `.git` subdirectory in it. */ +export const isRootGitRepo = (dir: string): boolean => { + return fs.existsSync(path.join(dir, ".git")); +}; + +/** @returns Whether or not this directory or a parent directory has a `.git` directory. */ +export const isInsideGitRepo = async (dir: string): Promise => { + try { + // If this command succeeds, we're inside a git repo + await execa("git", ["rev-parse", "--is-inside-work-tree"], { + cwd: dir, + stdout: "ignore", + }); + return true; + } catch (_e) { + // Else, it will throw a git-error and we return false + return false; + } +}; + +const getGitVersion = () => { + const stdout = execSync("git --version").toString().trim(); + const gitVersionTag = stdout.split(" ")[2]; + const major = gitVersionTag?.split(".")[0]; + const minor = gitVersionTag?.split(".")[1]; + return { major: Number(major), minor: Number(minor) }; +}; + +/** @returns The git config value of "init.defaultBranch". If it is not set, returns "main". */ +const getDefaultBranch = () => { + const stdout = execSync("git config --global init.defaultBranch || echo main").toString().trim(); + + return stdout; +}; + +// This initializes the Git-repository for the project +export const initializeGit = async (projectDir: string) => { + logger.info("Initializing Git..."); + + if (!isGitInstalled(projectDir)) { + logger.warn("Git is not installed. Skipping Git initialization."); + return; + } + + const spinner = ora("Creating a new git repo...\n").start(); + + const isRoot = isRootGitRepo(projectDir); + const isInside = await isInsideGitRepo(projectDir); + const dirName = path.parse(projectDir).name; // skip full path for logging + + if (isInside && isRoot) { + // Dir is a root git repo + spinner.stop(); + if (isNonInteractiveMode()) { + throw new Error( + `Cannot initialize git in non-interactive mode because "${dirName}" already contains a git repository.`, + ); + } + const overwriteGit = await p.confirm({ + message: `${chalk.redBright.bold( + "Warning:", + )} Git is already initialized in "${dirName}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`, + initialValue: false, + }); + + if (!overwriteGit) { + spinner.info("Skipping Git initialization."); + return; + } + // Deleting the .git folder + fs.removeSync(path.join(projectDir, ".git")); + } else if (isInside && !isRoot) { + // Dir is inside a git worktree + spinner.stop(); + if (isNonInteractiveMode()) { + throw new Error( + `Cannot initialize git in non-interactive mode because "${dirName}" is already inside a git worktree.`, + ); + } + const initializeChildGitRepo = await p.confirm({ + message: `${chalk.redBright.bold( + "Warning:", + )} "${dirName}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`, + initialValue: false, + }); + if (!initializeChildGitRepo) { + spinner.info("Skipping Git initialization."); + return; + } + } + + // We're good to go, initializing the git repo + try { + const branchName = getDefaultBranch(); + + // --initial-branch flag was added in git v2.28.0 + const { major, minor } = getGitVersion(); + if (major < 2 || (major === 2 && minor < 28)) { + await execa("git", ["init"], { cwd: projectDir }); + // symbolic-ref is used here due to refs/heads/master not existing + // It is only created after the first commit + // https://superuser.com/a/1419674 + await execa("git", ["symbolic-ref", "HEAD", `refs/heads/${branchName}`], { + cwd: projectDir, + }); + } else { + await execa("git", ["init", `--initial-branch=${branchName}`], { + cwd: projectDir, + }); + } + await execa("git", ["add", "."], { cwd: projectDir }); + await execa("git", ["commit", "-m", "Initial commit"], { + cwd: projectDir, + }); + spinner.succeed(`${chalk.green("Successfully initialized and staged")} ${chalk.green.bold("git")}\n`); + } catch (_error) { + // Safeguard, should be unreachable + spinner.fail(`${chalk.bold.red("Failed:")} could not initialize git. Update git to the latest version!\n`); + } +}; diff --git a/packages/cli-old/src/helpers/installDependencies.ts b/packages/cli-old/src/helpers/installDependencies.ts new file mode 100644 index 00000000..880bd436 --- /dev/null +++ b/packages/cli-old/src/helpers/installDependencies.ts @@ -0,0 +1,242 @@ +import chalk from "chalk"; +import { execa, type StdoutStderrOption } from "execa"; +import ora, { type Ora } from "ora"; + +import { state } from "~/state.js"; +import { getUserPkgManager, type PackageManager } from "~/utils/getUserPkgManager.js"; +import { logger } from "~/utils/logger.js"; + +const execWithSpinner = async ( + projectDir: string, + pkgManager: PackageManager | "pnpx" | "bunx", + options: { + args?: string[]; + stdout?: StdoutStderrOption; + onDataHandle?: (spinner: Ora) => (data: Buffer) => void; + loadingMessage?: string; + }, +) => { + const { onDataHandle, args = ["install"], stdout = "pipe" } = options; + + if (process.env.PROOFKIT_ENV === "development") { + args.push("--prefer-offline"); + } + + const spinner = ora(options.loadingMessage ?? `Running ${pkgManager} ${args.join(" ")} ...`).start(); + const subprocess = execa(pkgManager, args, { + cwd: projectDir, + stdout, + stderr: "pipe", // Capture stderr to get error messages + }); + + await new Promise((res, rej) => { + let stdoutOutput = ""; + let stderrOutput = ""; + + if (onDataHandle) { + subprocess.stdout?.on("data", onDataHandle(spinner)); + } else { + // If no custom handler, capture stdout for error reporting + subprocess.stdout?.on("data", (data) => { + stdoutOutput += data.toString(); + }); + } + + // Capture stderr output for error reporting + subprocess.stderr?.on("data", (data) => { + stderrOutput += data.toString(); + }); + + subprocess.on("error", (e) => rej(e)); + subprocess.on("close", (code) => { + if (code === 0) { + res(); + } else { + // Combine stdout and stderr for complete error message + const combinedOutput = [stdoutOutput, stderrOutput] + .filter((output) => output.trim()) + .join("\n") + .trim() + // Remove spinner-related lines that aren't useful in error output + .replace(/^- Checking registry\.$/gm, "") + .replace(/^\s*$/gm, "") // Remove empty lines + .trim(); + + const errorMessage = combinedOutput || `Command failed with exit code ${code}: ${pkgManager} ${args.join(" ")}`; + rej(new Error(errorMessage)); + } + }); + }); + + return spinner; +}; + +const runInstallCommand = async (pkgManager: PackageManager, projectDir: string): Promise => { + switch (pkgManager) { + // When using npm, inherit the stderr stream so that the progress bar is shown + case "npm": + await execa(pkgManager, ["install"], { + cwd: projectDir, + stderr: "inherit", + }); + + return null; + // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress + case "pnpm": + return execWithSpinner(projectDir, pkgManager, { + onDataHandle: (spinner) => (data) => { + const text = data.toString(); + + if (text.includes("Progress")) { + spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text; + } + }, + }); + case "yarn": + return execWithSpinner(projectDir, pkgManager, { + onDataHandle: (spinner) => (data) => { + spinner.text = data.toString(); + }, + }); + // When using bun, the stdout stream is ignored and the spinner is shown + case "bun": + return execWithSpinner(projectDir, pkgManager, { stdout: "ignore" }); + default: + throw new Error(`Unknown package manager: ${pkgManager}`); + } +}; + +export const installDependencies = async (args?: { projectDir?: string }) => { + const { projectDir = state.projectDir } = args ?? {}; + logger.info("Installing dependencies..."); + const pkgManager = getUserPkgManager(); + + const installSpinner = await runInstallCommand(pkgManager, projectDir); + + // If the spinner was used to show the progress, use succeed method on it + // If not, use the succeed on a new spinner + (installSpinner ?? ora()).succeed(chalk.green("Successfully installed dependencies!\n")); +}; + +export const runExecCommand = async ({ + command, + projectDir = state.projectDir, + successMessage, + errorMessage, + loadingMessage, +}: { + command: string[]; + projectDir?: string; + successMessage?: string; + errorMessage?: string; + loadingMessage?: string; +}) => { + let spinner: Ora | null = null; + + try { + spinner = await _runExecCommand({ + projectDir, + command, + loadingMessage, + }); + + // If the spinner was used to show the progress, use succeed method on it + // If not, use the succeed on a new spinner + (spinner ?? ora()).succeed( + chalk.green(successMessage ? `${successMessage}\n` : `Successfully ran ${command.join(" ")}!\n`), + ); + } catch (error) { + // If we have a spinner, fail it, otherwise just throw the error + if (spinner) { + const failMessage = errorMessage || `Failed to run ${command.join(" ")}`; + spinner.fail(chalk.red(failMessage)); + } + throw error; + } +}; + +export const _runExecCommand = async ({ + projectDir, + command, + loadingMessage, +}: { + projectDir: string; + exec?: boolean; + command: string[]; + loadingMessage?: string; +}): Promise => { + const pkgManager = getUserPkgManager(); + switch (pkgManager) { + // When using npm, capture both stdout and stderr to show error messages + case "npm": { + const result = await execa("npx", [...command], { + cwd: projectDir, + stdout: "pipe", + stderr: "pipe", + reject: false, + }); + + if (result.exitCode !== 0) { + // Combine stdout and stderr for complete error message + const combinedOutput = [result.stdout, result.stderr] + .filter((output) => output?.trim()) + .join("\n") + .trim() + // Remove spinner-related lines that aren't useful in error output + .replace(/^- Checking registry\.$/gm, "") + .replace(/^\s*$/gm, "") // Remove empty lines + .trim(); + + const errorMessage = + combinedOutput || `Command failed with exit code ${result.exitCode}: npx ${command.join(" ")}`; + throw new Error(errorMessage); + } + + return null; + } + // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress + case "pnpm": { + // For shadcn commands, don't use progress handler to capture full output + const isInstallCommand = command.includes("install"); + return execWithSpinner(projectDir, "pnpm", { + args: ["dlx", ...command], + loadingMessage, + onDataHandle: isInstallCommand + ? (spinner) => (data) => { + const text = data.toString(); + + if (text.includes("Progress")) { + spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text; + } + } + : undefined, + }); + } + case "yarn": { + // For shadcn commands, don't use progress handler to capture full output + const isYarnInstallCommand = command.includes("install"); + return execWithSpinner(projectDir, pkgManager, { + args: [...command], + loadingMessage, + onDataHandle: isYarnInstallCommand + ? (spinner) => (data) => { + spinner.text = data.toString(); + } + : undefined, + }); + } + // When using bun, the stdout stream is ignored and the spinner is shown + case "bun": + return execWithSpinner(projectDir, "bunx", { + stdout: "ignore", + args: [...command], + loadingMessage, + }); + default: + throw new Error(`Unknown package manager: ${pkgManager}`); + } +}; + +export function generateRandomSecret(): string { + return crypto.randomUUID().replace(/-/g, ""); +} diff --git a/packages/cli-old/src/helpers/installPackages.ts b/packages/cli-old/src/helpers/installPackages.ts new file mode 100644 index 00000000..06345c47 --- /dev/null +++ b/packages/cli-old/src/helpers/installPackages.ts @@ -0,0 +1,25 @@ +import type { InstallerOptions, PkgInstallerMap } from "~/installers/index.js"; +import { logger } from "~/utils/logger.js"; + +type InstallPackagesOptions = InstallerOptions & { + packages: PkgInstallerMap; +}; +// This runs the installer for all the packages that the user has selected +export const installPackages = (options: InstallPackagesOptions) => { + const { packages } = options; + logger.info("Adding boilerplate..."); + + for (const [_name, pkgOpts] of Object.entries(packages)) { + if (pkgOpts.inUse) { + // const spinner = ora(`Boilerplating ${name}...`).start(); + pkgOpts.installer(options); + // spinner.succeed( + // chalk.green( + // `Successfully setup boilerplate for ${chalk.green.bold(name)}` + // ) + // ); + } + } + + logger.info(""); +}; diff --git a/packages/cli-old/src/helpers/logNextSteps.ts b/packages/cli-old/src/helpers/logNextSteps.ts new file mode 100644 index 00000000..5b7c845d --- /dev/null +++ b/packages/cli-old/src/helpers/logNextSteps.ts @@ -0,0 +1,48 @@ +import chalk from "chalk"; + +import { DEFAULT_APP_NAME } from "~/consts.js"; +import type { InstallerOptions } from "~/installers/index.js"; +import { state } from "~/state.js"; +import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; +import { logger } from "~/utils/logger.js"; + +const formatRunCommand = (pkgManager: ReturnType, command: string) => + ["npm", "bun"].includes(pkgManager) ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`; + +// This logs the next steps that the user should take in order to advance the project +export const logNextSteps = ({ + projectName = DEFAULT_APP_NAME, + noInstall, +}: Pick) => { + const pkgManager = getUserPkgManager(); + + logger.info(chalk.bold("Next steps:")); + logger.dim("\nNavigate to the project directory:"); + projectName !== "." && logger.info(` cd ${projectName}`); + logger.dim("(or open in your code editor, and run the rest of these commands from there)"); + + if (noInstall) { + logger.dim("\nInstall dependencies:"); + // To reflect yarn's default behavior of installing packages when no additional args provided + if (pkgManager === "yarn") { + logger.info(` ${pkgManager}`); + } else { + logger.info(` ${pkgManager} install`); + } + } + + logger.dim("\nStart the dev server to view your app in a browser:"); + logger.info(` ${formatRunCommand(pkgManager, "dev")}`); + + if (state.appType === "webviewer") { + logger.dim("\nWhen you're ready to generate FileMaker clients:"); + logger.info(` ${formatRunCommand(pkgManager, "typegen")}`); + + logger.dim("\nTo open the starter inside FileMaker once your file is ready:"); + logger.info(` ${formatRunCommand(pkgManager, "launch-fm")}`); + } + + logger.dim("\nOr, run the ProofKit command again to add more to your project:"); + logger.info(` ${formatRunCommand(pkgManager, "proofkit")}`); + logger.dim("(Must be inside the project directory)"); +}; diff --git a/packages/cli-old/src/helpers/replaceText.ts b/packages/cli-old/src/helpers/replaceText.ts new file mode 100644 index 00000000..e7f9d4b1 --- /dev/null +++ b/packages/cli-old/src/helpers/replaceText.ts @@ -0,0 +1,17 @@ +import fs from "node:fs"; +import path from "node:path"; + +export function replaceTextInFiles(directoryPath: string, search: string, replacement: string): void { + const files = fs.readdirSync(directoryPath); + + for (const file of files) { + const filePath = path.join(directoryPath, file); + if (fs.statSync(filePath).isDirectory()) { + replaceTextInFiles(filePath, search, replacement); + } else { + const data = fs.readFileSync(filePath, "utf8"); + const updatedData = data.replace(new RegExp(search, "g"), replacement); + fs.writeFileSync(filePath, updatedData, "utf8"); + } + } +} diff --git a/packages/cli-old/src/helpers/scaffoldProject.ts b/packages/cli-old/src/helpers/scaffoldProject.ts new file mode 100644 index 00000000..7905eb0a --- /dev/null +++ b/packages/cli-old/src/helpers/scaffoldProject.ts @@ -0,0 +1,136 @@ +import path from "node:path"; +import chalk from "chalk"; +import fs from "fs-extra"; +import ora from "ora"; +import * as p from "~/cli/prompts.js"; + +import { PKG_ROOT } from "~/consts.js"; +import type { InstallerOptions } from "~/installers/index.js"; +import { isNonInteractiveMode, state } from "~/state.js"; +import { logger } from "~/utils/logger.js"; + +const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]); + +function getMeaningfulDirectoryEntries(projectDir: string): string[] { + return fs.readdirSync(projectDir).filter((entry) => { + if (AGENT_METADATA_DIRS.has(entry)) { + return false; + } + + if (entry === ".gitignore") { + return true; + } + + if (entry.startsWith(".")) { + return false; + } + + return true; + }); +} + +// This bootstraps the base Next.js application +export const scaffoldProject = async ({ + projectName, + pkgManager, + noInstall, + force = false, +}: InstallerOptions & { force?: boolean }) => { + const projectDir = state.projectDir; + + const srcDir = path.join( + PKG_ROOT, + state.appType === "browser" + ? `template/${state.ui === "mantine" ? "nextjs-mantine" : "nextjs-shadcn"}` + : "template/vite-wv", + ); + + if (noInstall) { + logger.info(""); + } else { + logger.info(`\nUsing: ${chalk.cyan.bold(pkgManager)}\n`); + } + + const spinner = ora(`Scaffolding in: ${projectDir}...\n`).start(); + + if (fs.existsSync(projectDir)) { + const meaningfulEntries = getMeaningfulDirectoryEntries(projectDir); + + if (meaningfulEntries.length === 0) { + if (projectName !== ".") { + spinner.info(`${chalk.cyan.bold(projectName)} exists but is empty, continuing...\n`); + } + } else if (force) { + spinner.info( + `${chalk.yellow("Force mode enabled:")} clearing ${chalk.cyan.bold(projectName)} before scaffolding...\n`, + ); + fs.emptyDirSync(projectDir); + spinner.start(); + // continue to scaffold after clearing + } else if (isNonInteractiveMode()) { + spinner.fail( + `${chalk.redBright.bold("Error:")} ${chalk.cyan.bold( + projectName, + )} already exists and isn't empty. Remove the existing files or choose a different directory.`, + ); + throw new Error( + `Cannot initialize into a non-empty directory in non-interactive mode: ${meaningfulEntries.join(", ")}`, + ); + } else { + spinner.stopAndPersist(); + const overwriteDir = await p.select({ + message: `${chalk.redBright.bold("Warning:")} ${chalk.cyan.bold( + projectName, + )} already exists and isn't empty. How would you like to proceed?`, + options: [ + { + label: "Abort installation (recommended)", + value: "abort", + }, + { + label: "Clear the directory and continue installation", + value: "clear", + }, + { + label: "Continue installation and overwrite conflicting files", + value: "overwrite", + }, + ], + initialValue: "abort", + }); + if (overwriteDir === "abort") { + spinner.fail("Aborting installation..."); + process.exit(1); + } + + const overwriteAction = overwriteDir === "clear" ? "clear the directory" : "overwrite conflicting files"; + + const confirmOverwriteDir = await p.confirm({ + message: `Are you sure you want to ${overwriteAction}?`, + initialValue: false, + }); + + if (!confirmOverwriteDir) { + spinner.fail("Aborting installation..."); + process.exit(1); + } + + if (overwriteDir === "clear") { + spinner.info(`Emptying ${chalk.cyan.bold(projectName)} and creating new ProofKit app..\n`); + fs.emptyDirSync(projectDir); + } + } + } + + spinner.start(); + + // Copy the main template + fs.copySync(srcDir, projectDir); + + // Rename gitignore + fs.renameSync(path.join(projectDir, "_gitignore"), path.join(projectDir, ".gitignore")); + + const scaffoldedName = projectName === "." ? "App" : chalk.cyan.bold(projectName); + + spinner.succeed(`${scaffoldedName} ${chalk.green("scaffolded successfully!")}\n`); +}; diff --git a/packages/cli-old/src/helpers/selectBoilerplate.ts b/packages/cli-old/src/helpers/selectBoilerplate.ts new file mode 100644 index 00000000..4b538d3d --- /dev/null +++ b/packages/cli-old/src/helpers/selectBoilerplate.ts @@ -0,0 +1,32 @@ +import path from "node:path"; +import fs from "fs-extra"; + +import { PKG_ROOT } from "~/consts.js"; +import type { InstallerOptions } from "~/installers/index.js"; +import { state } from "~/state.js"; + +type SelectBoilerplateProps = Required>; + +export const selectLayoutFile = (_props: SelectBoilerplateProps) => { + const projectDir = state.projectDir; + const layoutFileDir = path.join(PKG_ROOT, "template/extras/src/app/layout"); + + const layoutFile = "base.tsx"; + + const appSrc = path.join(layoutFileDir, layoutFile); // base layout + const appDest = path.join(projectDir, "src/app/layout.tsx"); + fs.copySync(appSrc, appDest); + + fs.copySync(path.join(layoutFileDir, "main-shell.tsx"), path.join(projectDir, "src/app/(main)/layout.tsx")); +}; + +export const selectPageFile = (_props: SelectBoilerplateProps) => { + const projectDir = state.projectDir; + const indexFileDir = path.join(PKG_ROOT, "template/extras/src/app/page"); + + const indexFile = "base.tsx"; + + const indexSrc = path.join(indexFileDir, indexFile); + const indexDest = path.join(projectDir, "src/app/(main)/page.tsx"); + fs.copySync(indexSrc, indexDest); +}; diff --git a/packages/cli-old/src/helpers/setImportAlias.ts b/packages/cli-old/src/helpers/setImportAlias.ts new file mode 100644 index 00000000..7551134b --- /dev/null +++ b/packages/cli-old/src/helpers/setImportAlias.ts @@ -0,0 +1,12 @@ +import { replaceTextInFiles } from "./replaceText.js"; + +const TRAILING_SLASH_REGEX = /[^/]$/; + +export const setImportAlias = (projectDir: string, importAlias: string) => { + const normalizedImportAlias = importAlias + .replace(/\*/g, "") // remove any wildcards (~/* -> ~/) + .replace(TRAILING_SLASH_REGEX, "$&/"); // ensure trailing slash (@ -> ~/) + + // update import alias in any files if not using the default + replaceTextInFiles(projectDir, "~/", normalizedImportAlias); +}; diff --git a/packages/cli-old/src/helpers/shadcn-cli.ts b/packages/cli-old/src/helpers/shadcn-cli.ts new file mode 100644 index 00000000..4c235380 --- /dev/null +++ b/packages/cli-old/src/helpers/shadcn-cli.ts @@ -0,0 +1,80 @@ +import fs from "node:fs"; +import path from "node:path"; +import { execa } from "execa"; + +import { DEFAULT_REGISTRY_URL } from "~/consts.js"; +import { state } from "~/state.js"; +import { logger } from "~/utils/logger.js"; +import { getSettings } from "~/utils/parseSettings.js"; + +export async function shadcnInstall(components: string | string[], _friendlyComponentName?: string) { + const componentsArray = Array.isArray(components) ? components : [components]; + const command = ["shadcn@latest", "add", ...componentsArray]; + // Use execa to run the shadcn add command directly + + try { + await execa("pnpm", ["dlx", ...command], { + stdio: "inherit", + cwd: state.projectDir ?? process.cwd(), + }); + } catch (error) { + logger.error(`Failed to run shadcn add: ${error}`); + throw error; + } +} + +export function getRegistryUrl(): string { + let url: string; + try { + url = getSettings().registryUrl ?? DEFAULT_REGISTRY_URL; + } catch { + // If we can't get settings (e.g., during development or outside a ProofKit project), + // fall back to the default registry URL + url = DEFAULT_REGISTRY_URL; + } + return url.endsWith("/") ? url.slice(0, -1) : url; +} + +export interface ShadcnConfig { + style: "default" | "new-york"; + tailwind: { + config: string; + css: string; + baseColor: string; + cssVariables: boolean; + prefix?: string; + [k: string]: unknown; + }; + rsc: boolean; + tsx?: boolean; + iconLibrary?: string; + aliases: { + utils: string; + components: string; + ui?: string; + lib?: string; + hooks?: string; + [k: string]: unknown; + }; + registries?: { + [k: string]: + | string + | { + url: string; + params?: { + [k: string]: string; + }; + headers?: { + [k: string]: string; + }; + [k: string]: unknown; + }; + }; + [k: string]: unknown; +} + +export function getShadcnConfig() { + const componentsJsonPath = path.join(state.projectDir, "components.json"); + const componentsJson = JSON.parse(fs.readFileSync(componentsJsonPath, "utf8")); + return componentsJson as ShadcnConfig; +} diff --git a/packages/cli-old/src/helpers/stealth-init.ts b/packages/cli-old/src/helpers/stealth-init.ts new file mode 100644 index 00000000..6f865ab8 --- /dev/null +++ b/packages/cli-old/src/helpers/stealth-init.ts @@ -0,0 +1,20 @@ +import fs from "fs-extra"; + +import { defaultSettings, setSettings, validateAndSetEnvFile } from "~/utils/parseSettings.js"; + +/** + * Used to add a proofkit.json file to an existing project + */ +export async function stealthInit() { + // check if proofkit.json exists + const proofkitJson = await fs.pathExists("proofkit.json"); + if (proofkitJson) { + return; + } + + // create proofkit.json with default settings + setSettings(defaultSettings); + + // validate and set envFile only if it exists + validateAndSetEnvFile(); +} diff --git a/packages/cli-old/src/helpers/version-fetcher.ts b/packages/cli-old/src/helpers/version-fetcher.ts new file mode 100644 index 00000000..26a21e80 --- /dev/null +++ b/packages/cli-old/src/helpers/version-fetcher.ts @@ -0,0 +1,131 @@ +import https from "node:https"; +import { TRPCError } from "@trpc/server"; +import axios from "axios"; +import z from "zod/v4"; + +export async function fetchServerVersions({ url, ottoPort = 3030 }: { url: string; ottoPort?: number }) { + const fmsInfo = await fetchFMSVersionInfo(url); + const ottoInfo = await fetchOttoVersion({ url, ottoPort }); + return { fmsInfo, ottoInfo }; +} + +const fmsInfoSchema = z.object({ + data: z.object({ + APIVersion: z.number().optional(), + AcceptEARPassword: z.boolean().optional(), + AcceptEncrypted: z.boolean().optional(), + AcceptUnencrypted: z.boolean().optional(), + AdminLocalAuth: z.string().optional(), + AllowChangeUploadDBFolder: z.boolean().optional(), + AutoOpenForUpload: z.boolean().optional(), + DenyGuestAndAutoLogin: z.string().optional(), + Hostname: z.string().optional(), + IsAppleInternal: z.boolean().optional(), + IsETS: z.boolean().optional(), + PremisesType: z.string().optional(), + ProductVersion: z.string().optional(), + PublicKey: z.string().optional(), + RequiresDBPasswords: z.boolean().optional(), + ServerID: z.string().optional(), + ServerVersion: z.string(), + }), + result: z.number(), +}); + +export async function fetchFMSVersionInfo(url: string) { + const fmsUrl = new URL(url); + fmsUrl.pathname = "/fmws/serverinfo"; + + const fmsInfoResult = await fetchWithoutSSL(fmsUrl.toString()).then((r) => fmsInfoSchema.safeParse(r.data)); + if (!fmsInfoResult.success) { + console.error("fmsInfoResult.error", fmsInfoResult.error.issues); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Invalid FileMaker Server URL", + }); + } + return fmsInfoResult.data.data; +} + +const ottoInfoSchema = z.object({ + Otto: z.object({ + version: z.string(), + serverNickname: z.string().default(""), + isLicenseValid: z.boolean().optional(), + }), + migratorVersion: z.string().optional(), + FileMakerServer: z.object({ + version: z.object({ + long: z.string(), + short: z.string(), + }), + running: z.boolean().optional(), + }), + isMac: z.boolean().optional(), + platform: z.string().optional(), + host: z.string().optional(), +}); + +const ottoInfoResponseSchema = z.object({ + response: ottoInfoSchema, +}); + +export async function fetchOttoVersion({ + url, + ottoPort = 3030, +}: { + url: string; + ottoPort?: number | null; +}): Promise | null> { + let ottoInfo = await fetchOtto4Version(url); + if (!ottoInfo) { + ottoInfo = await fetchOtto3Version(url, ottoPort); + } + return ottoInfo; +} + +async function fetchOtto4Version(url: string) { + try { + const otto4Url = new URL(url); + otto4Url.pathname = "/otto/api/info"; + const otto4Info = await fetchWithoutSSL(otto4Url.toString()).then((r) => { + return ottoInfoResponseSchema.parse(r.data).response; + }); + return otto4Info; + } catch (_error) { + console.log("unable to fetch otto4 info, trying otto3"); + return null; + } +} + +async function fetchOtto3Version(url: string, ottoPort: number | null) { + try { + const otto3Url = new URL(url); + otto3Url.port = ottoPort ? ottoPort.toString() : "3030"; + otto3Url.pathname = "/api/otto/info"; + const ottoInfo = await fetchWithoutSSL(otto3Url.toString()).then((res) => { + return ottoInfoSchema.parse(res.data); + }); + return ottoInfo; + } catch (error) { + if (error instanceof Error) { + console.error("otto3 fetch error", error.message); + } + return null; + } +} + +async function fetchWithoutSSL(url: string) { + const agent = new https.Agent({ + rejectUnauthorized: false, + }); + + const result = await axios.get(url, { + validateStatus: null, + headers: { Connection: "close" }, + httpsAgent: agent, + timeout: 10_000, + }); + + return result; +} diff --git a/packages/cli-old/src/index.ts b/packages/cli-old/src/index.ts new file mode 100644 index 00000000..a61c41c7 --- /dev/null +++ b/packages/cli-old/src/index.ts @@ -0,0 +1,96 @@ +#!/usr/bin/env node --no-warnings +import chalk from "chalk"; +import { Command } from "commander"; +import { makeInitCommand, runInit } from "~/cli/init.js"; +import { intro } from "~/cli/prompts.js"; +import { logger } from "~/utils/logger.js"; +import { proofGradient, renderTitle } from "~/utils/renderTitle.js"; +import { makeAddCommand } from "./cli/add/index.js"; +import { makeDeployCommand } from "./cli/deploy/index.js"; +import { runMenu } from "./cli/menu.js"; +import { makeRemoveCommand } from "./cli/remove/index.js"; +import { makeTypegenCommand } from "./cli/typegen/index.js"; +import { makeUpgradeCommand } from "./cli/update/makeUpgradeCommand.js"; +import { UserAbortedError } from "./cli/utils.js"; +import { npmName } from "./consts.js"; +import { ciOption, nonInteractiveOption } from "./globalOptions.js"; +import { initProgramState, isNonInteractiveMode } from "./state.js"; +import { getVersion } from "./utils/getProofKitVersion.js"; +import { getSettings, type Settings } from "./utils/parseSettings.js"; +import { checkAndRenderVersionWarning } from "./utils/renderVersionWarning.js"; + +const version = getVersion(); + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +const main = async () => { + const program = new Command(); + renderTitle(); + if (process.env.PROOFKIT_SKIP_VERSION_CHECK !== "1") { + await checkAndRenderVersionWarning(); + } + + program + .name(npmName) + .version(version) + .command("default", { hidden: true, isDefault: true }) + .addOption(ciOption) + .addOption(nonInteractiveOption) + .action(async (args) => { + initProgramState(args); + + let settings: Settings | undefined; + try { + settings = getSettings(); + } catch { + // void + } + + if (isNonInteractiveMode()) { + throw new Error( + "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit init --non-interactive`.", + ); + } + + if (settings) { + intro(`Found ${proofGradient("ProofKit")} project`); + await runMenu(); + } else { + intro(`No ${proofGradient("ProofKit")} project found, running \`init\``); + await runInit(); + } + }) + .addHelpText("afterAll", `\n The ProofKit CLI was inspired by the ${chalk.hex("#E8DCFF").bold("t3 stack")}\n`); + + program.addCommand(makeInitCommand()); + program.addCommand(makeAddCommand()); + program.addCommand(makeRemoveCommand()); + program.addCommand(makeTypegenCommand()); + program.addCommand(makeDeployCommand()); + program.addCommand(makeUpgradeCommand()); + + await program.parseAsync(process.argv); + process.exit(0); +}; + +main().catch((err) => { + if (err instanceof UserAbortedError) { + process.exit(0); + } else if (err instanceof Error) { + logger.error("Aborting installation..."); + logger.error(err.message); + const cause = (err as Error & { cause?: unknown }).cause; + if (cause) { + logger.dim(`Cause: ${getErrorMessage(cause)}`); + } + } else { + logger.error("An unknown error has occurred. Please open an issue on github with the below:"); + console.log(err); + } + process.exit(1); +}); diff --git a/packages/cli-old/src/installers/auth-shared.ts b/packages/cli-old/src/installers/auth-shared.ts new file mode 100644 index 00000000..20f1401d --- /dev/null +++ b/packages/cli-old/src/installers/auth-shared.ts @@ -0,0 +1,49 @@ +import { type SourceFile, SyntaxKind } from "ts-morph"; + +import { ensureReturnStatementIsWrappedInFragment } from "~/utils/ts-morph.js"; + +export function addToHeaderSlot(slotSourceFile: SourceFile, importFrom: string) { + slotSourceFile.addImportDeclaration({ + defaultImport: "UserMenu", + moduleSpecifier: importFrom, + }); + + // ensure Group from @mantine/core is imported + const mantineCoreImport = slotSourceFile.getImportDeclaration( + (dec) => dec.getModuleSpecifierValue() === "@mantine/core", + ); + if (mantineCoreImport) { + const groupImport = mantineCoreImport.getNamedImports().find((imp) => imp.getName() === "Group"); + + if (!groupImport) { + mantineCoreImport.addNamedImport({ name: "Group" }); + } + } else { + slotSourceFile.addImportDeclaration({ + namedImports: [{ name: "Group" }], + moduleSpecifier: "@mantine/core", + }); + } + + const returnStatement = ensureReturnStatementIsWrappedInFragment( + slotSourceFile + .getFunction((dec) => dec.isDefaultExport()) + ?.getBody() + ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement), + ); + + const existingElements = returnStatement + ?.getFirstDescendantByKind(SyntaxKind.JsxOpeningFragment) + ?.getParentIfKind(SyntaxKind.JsxFragment) + ?.getFirstDescendantByKind(SyntaxKind.SyntaxList) + ?.getText(); + + if (!existingElements) { + console.log(`Failed to inject into header slot at ${slotSourceFile.getFilePath()}`); + return; + } + + returnStatement?.replaceWithText(`return (<>${existingElements})`); + returnStatement?.formatText(); + slotSourceFile.saveSync(); +} diff --git a/packages/cli-old/src/installers/better-auth.ts b/packages/cli-old/src/installers/better-auth.ts new file mode 100644 index 00000000..f417ab36 --- /dev/null +++ b/packages/cli-old/src/installers/better-auth.ts @@ -0,0 +1,3 @@ +export async function betterAuthInstaller() { + // TODO: Implement better-auth installer +} diff --git a/packages/cli-old/src/installers/clerk.ts b/packages/cli-old/src/installers/clerk.ts new file mode 100644 index 00000000..11ddd816 --- /dev/null +++ b/packages/cli-old/src/installers/clerk.ts @@ -0,0 +1,153 @@ +import path from "node:path"; +import chalk from "chalk"; +import fs from "fs-extra"; +import { type SourceFile, SyntaxKind } from "ts-morph"; + +import { PKG_ROOT } from "~/consts.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { addToEnv } from "~/utils/addToEnvs.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; +import { addToHeaderSlot } from "./auth-shared.js"; + +export const clerkInstaller = async ({ projectDir }: { projectDir: string }) => { + addPackageDependency({ + projectDir, + dependencies: ["@clerk/nextjs", "@clerk/themes"], + devMode: false, + }); + + // add clerk middleware + // check if middleware already exists, if not add it + const extrasDir = path.join(PKG_ROOT, "template/extras"); + + const middlewareDest = path.join(projectDir, "src/middleware.ts"); + if (fs.existsSync(middlewareDest)) { + // throw new Error("Middleware already exists"); + console.log( + chalk.yellow( + "Middleware already exists. To require auth for your app, be sure to follow the guide to setup Clerk middleware. https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-next-js", + ), + ); + } else { + const middlewareSrc = path.join(extrasDir, "src/middleware/clerk.ts"); + fs.copySync(middlewareSrc, middlewareDest); + } + + // copy auth pages + fs.copySync(path.join(extrasDir, "src/app/clerk-auth"), path.join(projectDir, "src/app/auth")); + + // copy auth components + fs.copySync(path.join(extrasDir, "src/components/clerk-auth"), path.join(projectDir, "src/components/clerk-auth")); + + // add ClerkProvider to app layout + const layoutFile = path.join(projectDir, "src/app/layout.tsx"); + const project = getNewProject(projectDir); + addClerkProvider(project.addSourceFileAtPath(layoutFile)); + + // inject signin/signout components to header slots + addToHeaderSlot( + project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), + "@/components/clerk-auth/user-menu", + ); + addToHeaderSlot( + project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-mobile-content.tsx")), + "@/components/clerk-auth/user-menu-mobile", + ); + + addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); + + // add envs to .env and .env.schema + await addToEnv({ + projectDir, + project, + envs: [ + { + name: "NEXT_PUBLIC_CLERK_SIGN_IN_URL", + zodValue: "z.string()", + defaultValue: "/auth/signin", + type: "client", + }, + { + name: "NEXT_PUBLIC_CLERK_SIGN_UP_URL", + zodValue: "z.string()", + defaultValue: "/auth/signup", + type: "client", + }, + { + name: "CLERK_SECRET_KEY", + zodValue: `z.string().startsWith('sk_').min(1, { + message: + "No Clerk Secret Key found. Did you create your Clerk app and copy the environment variables to you .env file?", + })`, + type: "server", + }, + { + name: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", + zodValue: `z.string().startsWith('pk_').min(1, { + message: + "No Clerk Public Key found. Did you create your Clerk app and copy the environment variables to you .env file?", + })`, + type: "client", + }, + ], + envFileDescription: + "Hosted auth with Clerk. Set up a new app at https://dashboard.clerk.com/apps/new to get these values.", + }); + + await formatAndSaveSourceFiles(project); +}; + +export function addClerkProvider(sourceFile: SourceFile) { + sourceFile.addImportDeclaration({ + namedImports: [{ name: "ClerkAuthProvider" }], + moduleSpecifier: "@/components/clerk-auth/clerk-provider", + }); + + // Step 2: Wrap default exported function's return statement with ClerkProvider + const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport()); + + // find the mantine provider in this export + const mantineProvider = exportDefault + ?.getBody() + ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) + ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) + .find((openingElement) => openingElement.getTagNameNode().getText() === "MantineProvider") + ?.getParentIfKind(SyntaxKind.JsxElement); + + const childrenText = mantineProvider + ?.getJsxChildren() + .map((child) => child.getText()) + .filter(Boolean) + .join("\n"); + + mantineProvider?.getChildSyntaxList()?.replaceWithText( + ` + ${childrenText} + `, + ); +} + +function addToSafeActionClient(sourceFile?: SourceFile) { + if (!sourceFile) { + console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); + return; + } + + sourceFile.addImportDeclaration({ + namedImports: [{ name: "auth", alias: "getAuth" }], + moduleSpecifier: "@clerk/nextjs/server", + }); + + // add to end of file + sourceFile.addStatements((writer) => + writer.writeLine(`export const authedActionClient = actionClient.use(async ({ next, ctx }) => { + const auth = getAuth(); + if (!auth.userId) { + throw new Error("Unauthorized"); + } + return next({ ctx: { ...ctx, auth } }); +}); + +`), + ); +} diff --git a/packages/cli-old/src/installers/dependencyVersionMap.ts b/packages/cli-old/src/installers/dependencyVersionMap.ts new file mode 100644 index 00000000..c5b53268 --- /dev/null +++ b/packages/cli-old/src/installers/dependencyVersionMap.ts @@ -0,0 +1,108 @@ +import { getNodeMajorVersion } from "~/utils/getProofKitVersion.js"; +import { getProofkitReleaseTag } from "~/utils/proofkitReleaseChannel.js"; + +const proofkitReleaseTag = getProofkitReleaseTag(); + +/* + * This maps the necessary packages to a version. + * This improves performance significantly over fetching it from the npm registry. + */ +export const dependencyVersionMap = { + // Resolve to "latest" or "beta" based on current changeset state / versions. + "@proofkit/fmdapi": proofkitReleaseTag, + "@proofkit/webviewer": proofkitReleaseTag, + "@proofkit/cli": proofkitReleaseTag, + "@proofkit/typegen": proofkitReleaseTag, + "@proofkit/better-auth": proofkitReleaseTag, + + // NextAuth.js + "next-auth": "beta", + "next-auth-adapter-filemaker": "beta", + + "@auth/prisma-adapter": "^1.6.0", + "@auth/drizzle-adapter": "^1.1.0", + + // Prisma + prisma: "^5.14.0", + "@prisma/client": "^5.14.0", + "@prisma/adapter-planetscale": "^5.14.0", + + // Drizzle + "drizzle-orm": "^0.30.10", + "drizzle-kit": "^0.21.4", + mysql2: "^3.9.7", + "@planetscale/database": "^1.18.0", + postgres: "^3.4.4", + "@libsql/client": "^0.6.0", + + // TailwindCSS + tailwindcss: "^4.1.10", + postcss: "^8.4.41", + "@tailwindcss/postcss": "^4.1.10", + "@tailwindcss/vite": "^4.2.1", + "class-variance-authority": "^0.7.1", + clsx: "^2.1.1", + "tailwind-merge": "^3.5.0", + "tw-animate-css": "^1.4.0", + + // tRPC + "@trpc/client": "^11.0.0-rc.446", + "@trpc/server": "^11.0.0-rc.446", + "@trpc/react-query": "^11.0.0-rc.446", + "@trpc/next": "^11.0.0-rc.446", + superjson: "^2.2.1", + "server-only": "^0.0.1", + + // Clerk + "@clerk/nextjs": "^6.3.1", + "@clerk/themes": "^2.1.33", + + // Tanstack Query + "@tanstack/react-query": "^5.59.0", + "@tanstack/react-query-devtools": "^5.59.0", + + // ProofKit Auth + "@node-rs/argon2": "^2.0.2", + "@oslojs/binary": "^1.0.0", + "@oslojs/crypto": "^1.0.1", + "@oslojs/encoding": "^1.1.0", + "js-cookie": "^3.0.5", + "@types/js-cookie": "^3.0.6", + + // React Email + "@react-email/components": "^0.5.0", + "@react-email/render": "1.2.0", + "@react-email/preview-server": "^4.2.8", + "@plunk/node": "^3.0.3", + "react-email": "^4.2.8", + resend: "^4.0.0", + "@sendgrid/mail": "^8.1.4", + + // Node + "@types/node": `^${getNodeMajorVersion()}`, + + // Radix (for shadcn/ui) + "@radix-ui/react-slot": "^1.2.3", + + // Icons (for shadcn/ui) + "lucide-react": "^0.577.0", + + // better-auth + "better-auth": "^1.3.4", + "@daveyplate/better-auth-ui": "^2.1.3", + + // Mantine UI + "@mantine/core": "^7.15.0", + "@mantine/dates": "^7.15.0", + "@mantine/hooks": "^7.15.0", + "@mantine/modals": "^7.15.0", + "@mantine/notifications": "^7.15.0", + "mantine-react-table": "^2.0.0", + + // Theme utilities + "next-themes": "^0.4.6", + + // Zod + zod: "^4", +} as const; +export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/packages/cli-old/src/installers/envVars.ts b/packages/cli-old/src/installers/envVars.ts new file mode 100644 index 00000000..2eb95da9 --- /dev/null +++ b/packages/cli-old/src/installers/envVars.ts @@ -0,0 +1,43 @@ +import path from "node:path"; +import fs from "fs-extra"; + +import type { Installer } from "~/installers/index.js"; +import { state } from "~/state.js"; +import { logger } from "~/utils/logger.js"; + +export type FMAuthKeys = { username: string; password: string } | { ottoApiKey: string }; + +export const initEnvFile: Installer = () => { + const envFilePath = findT3EnvFile(false) ?? "./src/config/env.ts"; + + const envContent = ` +# When adding additional environment variables, the schema in "${envFilePath}" +# should be updated accordingly. + +` + .trim() + .concat("\n"); + + const envDest = path.join(state.projectDir, ".env"); + + fs.writeFileSync(envDest, envContent, "utf-8"); +}; +export function findT3EnvFile(throwIfNotFound: false): string | null; +export function findT3EnvFile(throwIfNotFound?: true): string; +export function findT3EnvFile(throwIfNotFound?: boolean): string | null { + const possiblePaths = ["src/config/env.ts", "src/lib/env.ts", "src/env.ts", "lib/env.ts", "env.ts", "config/env.ts"]; + + for (const testPath of possiblePaths) { + const fullPath = path.join(state.projectDir, testPath); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + if (throwIfNotFound === false) { + return null; + } + + logger.warn(`Could not find T3 env files. Run "proofkit add utils/t3-env" to initialize them.`); + throw new Error("T3 env file not found"); +} diff --git a/packages/cli-old/src/installers/index.ts b/packages/cli-old/src/installers/index.ts new file mode 100644 index 00000000..b4fb6fb6 --- /dev/null +++ b/packages/cli-old/src/installers/index.ts @@ -0,0 +1,31 @@ +import { initEnvFile } from "~/installers/envVars.js"; +import type { PackageManager } from "~/utils/getUserPkgManager.js"; + +// Turning this into a const allows the list to be iterated over for programmatically creating prompt options +// Should increase extensibility in the future +export const availablePackages = ["nextAuth", "trpc", "envVariables", "fmdapi", "webViewerFetch", "clerk"] as const; +export type AvailablePackages = (typeof availablePackages)[number]; + +export interface InstallerOptions { + pkgManager: PackageManager; + noInstall: boolean; + packages?: PkgInstallerMap; + projectName: string; + scopedAppName: string; +} + +export type Installer = (opts: InstallerOptions) => void; + +export type PkgInstallerMap = { + [pkg in AvailablePackages]?: { + inUse: boolean; + installer: Installer; + }; +}; + +export const buildPkgInstallerMap = (): PkgInstallerMap => ({ + envVariables: { + inUse: true, + installer: initEnvFile, + }, +}); diff --git a/packages/cli-old/src/installers/install-fm-addon.ts b/packages/cli-old/src/installers/install-fm-addon.ts new file mode 100644 index 00000000..384c3e16 --- /dev/null +++ b/packages/cli-old/src/installers/install-fm-addon.ts @@ -0,0 +1,53 @@ +import os from "node:os"; +import path from "node:path"; +import chalk from "chalk"; +import fs from "fs-extra"; + +import { PKG_ROOT } from "~/consts.js"; +import { logger } from "~/utils/logger.js"; + +export async function installFmAddon({ addonName }: { addonName: "auth" | "wv" }) { + const addonDisplayName = addonName === "auth" ? "FM Auth Add-on" : "ProofKit WebViewer"; + + let targetDir: string | null = null; + if (process.platform === "win32") { + targetDir = path.join(os.homedir(), "AppData", "Local", "FileMaker", "Extensions", "AddonModules"); + } else if (process.platform === "darwin") { + targetDir = path.join(os.homedir(), "Library", "Application Support", "FileMaker", "Extensions", "AddonModules"); + } + + if (!targetDir) { + logger.warn(`Could not install the ${addonDisplayName} addon. You will need to do this manually.`); + return; + } + + const addonDir = addonName === "auth" ? "ProofKitAuth" : "ProofKitWV"; + + await fs.copy(path.join(PKG_ROOT, `template/fm-addon/${addonDir}`), path.join(targetDir, addonDir), { + overwrite: true, + }); + + console.log(""); + console.log(chalk.bgYellow(" ACTION REQUIRED: ")); + if (addonName === "auth") { + console.log( + `${chalk.yellowBright( + "You must install the FM Auth addon in your FileMaker file to continue.", + )} ${chalk.dim("(Learn more: https://proofkit.dev/auth/fm-addon)")}`, + ); + } else { + console.log( + `${chalk.yellowBright( + "You must install the ProofKit WebViewer addon in your FileMaker file to continue.", + )} ${chalk.dim("(Learn more: https://proofkit.dev/webviewer)")}`, + ); + } + const steps = [ + "Restart FileMaker Pro (if it's currently running)", + `Open your FileMaker file, go to layout mode, and install the ${addonDisplayName} addon to the file`, + "Come back here to continue the installation", + ]; + steps.forEach((step, index) => { + console.log(`${index + 1}. ${step}`); + }); +} diff --git a/packages/cli-old/src/installers/nextAuth.ts b/packages/cli-old/src/installers/nextAuth.ts new file mode 100644 index 00000000..793f6d80 --- /dev/null +++ b/packages/cli-old/src/installers/nextAuth.ts @@ -0,0 +1,189 @@ +import path from "node:path"; +import chalk from "chalk"; +import fs from "fs-extra"; +import ora from "ora"; +import { type SourceFile, SyntaxKind } from "ts-morph"; + +import { PKG_ROOT } from "~/consts.js"; +import { getExistingSchemas } from "~/generators/fmdapi.js"; +import { _runExecCommand, generateRandomSecret } from "~/helpers/installDependencies.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { addToEnv } from "~/utils/addToEnvs.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; +import { addToHeaderSlot } from "./auth-shared.js"; +import { dependencyVersionMap } from "./dependencyVersionMap.js"; + +export const nextAuthInstaller = async ({ projectDir }: { projectDir: string }) => { + addPackageDependency({ + projectDir, + dependencies: ["next-auth", "next-auth-adapter-filemaker"], + devMode: false, + }); + + const extrasDir = path.join(PKG_ROOT, "template/extras"); + + const routeHandlerFile = "src/app/api/auth/[...nextauth]/route.ts"; + const srcToUse = routeHandlerFile; + + const apiHandlerSrc = path.join(extrasDir, srcToUse); + const apiHandlerDest = path.join(projectDir, srcToUse); + fs.copySync(apiHandlerSrc, apiHandlerDest); + + const authConfigSrc = path.join(extrasDir, "src/server", "next-auth", "base.ts"); + const authConfigDest = path.join(projectDir, "src/server/auth.ts"); + fs.copySync(authConfigSrc, authConfigDest); + + const passwordSrc = path.join(extrasDir, "src/server", "next-auth", "password.ts"); + const passwordDest = path.join(projectDir, "src/server/password.ts"); + fs.copySync(passwordSrc, passwordDest); + + // copy users.ts to data directory + fs.copySync(path.join(extrasDir, "src/server/data/users.ts"), path.join(projectDir, "src/server/data/users.ts")); + + // copy auth pages + fs.copySync(path.join(extrasDir, "src/app/next-auth"), path.join(projectDir, "src/app/auth")); + + // copy auth components + fs.copySync(path.join(extrasDir, "src/components/next-auth"), path.join(projectDir, "src/components/next-auth")); + + const project = getNewProject(projectDir); + + // modify root layout to wrap with session provider + addNextAuthProviderToRootLayout(project.addSourceFileAtPath(path.join(projectDir, "src/app/layout.tsx"))); + + // inject signin/signout components to header slots + addToHeaderSlot( + project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), + "@/components/next-auth/user-menu", + ); + addToHeaderSlot( + project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-mobile-content.tsx")), + "@/components/next-auth/user-menu-mobile", + ); + + // add a protected safe-action-client + addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); + + // // TODO do this part in-house, maybe with execa directly + // await runExecCommand({ + // command: ["auth", "secret"], + // projectDir, + // }); + + // add middleware + fs.copySync(path.join(extrasDir, "src/middleware/next-auth.ts"), path.join(projectDir, "src/middleware.ts")); + + // add envs to .env and .env.schema + await addToEnv({ + projectDir, + project, + envs: [ + { + name: "AUTH_SECRET", + zodValue: "z.string().min(1)", + defaultValue: generateRandomSecret(), + type: "server", + }, + ], + }); + + await checkForNextAuthLayouts(projectDir); + + await formatAndSaveSourceFiles(project); +}; + +function addNextAuthProviderToRootLayout(rootLayoutSource: SourceFile) { + // Add imports + rootLayoutSource.addImportDeclaration({ + namedImports: [{ name: "NextAuthProvider" }], + moduleSpecifier: "@/components/next-auth/next-auth-provider", + }); + rootLayoutSource.addImportDeclaration({ + namedImports: [{ name: "auth" }], + moduleSpecifier: "@/server/auth", + }); + + const exportDefault = rootLayoutSource.getFunction((dec) => dec.isDefaultExport()); + + // make the function async + exportDefault?.setIsAsync(true); + + // get the session server-side + exportDefault?.getFirstDescendantByKind(SyntaxKind.Block)?.insertStatements(0, "const session = await auth();"); + + // get the body element from the return statement + const bodyElement = exportDefault + ?.getBody() + ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) + ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) + .find((openingElement) => openingElement.getTagNameNode().getText() === "body") + ?.getParentIfKind(SyntaxKind.JsxElement); + + // wrap the body element with the next auth provider + bodyElement?.replaceWithText( + ` + ${bodyElement.getText()} + `, + ); + + rootLayoutSource.formatText(); + rootLayoutSource.saveSync(); +} + +function addToSafeActionClient(sourceFile?: SourceFile) { + if (!sourceFile) { + console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); + return; + } + + sourceFile.addImportDeclaration({ + namedImports: [{ name: "auth" }], + moduleSpecifier: "@/server/auth", + }); + + // add to end of file + sourceFile.addStatements((writer) => + writer.writeLine(`export const authedActionClient = actionClient.use( + async ({ next, ctx }) => { + const session = await auth(); + if (!session) { + throw new Error("Unauthorized"); + } + return next({ ctx: { ...ctx, session } }); + } +); +`), + ); +} + +async function checkForNextAuthLayouts(projectDir: string) { + const existingLayouts = getExistingSchemas({ + projectDir, + dataSourceName: "filemaker", + }); + const nextAuthLayouts = ["nextauth_user", "nextauth_account", "nextauth_session", "nextauth_verificationToken"]; + + const allNextAuthLayoutsExist = nextAuthLayouts.every((layout) => + existingLayouts.some((l) => l.schemaName === layout), + ); + + if (allNextAuthLayoutsExist) { + return; + } + + const spinner = await _runExecCommand({ + command: [`next-auth-adapter-filemaker@${dependencyVersionMap["next-auth-adapter-filemaker"]}`, "install-addon"], + projectDir, + }); + + // If the spinner was used to show the progress, use succeed method on it + // If not, use the succeed on a new spinner + (spinner ?? ora()).succeed(chalk.green("Successfully installed next-auth addon for FileMaker")); + + console.log(""); + console.log(chalk.bgYellow(" ACTION REQUIRED: ")); + console.log( + `${chalk.yellowBright("You must now install the NextAuth addon in your FileMaker file.")} +Learn more: https://proofkit.dev/auth/next-auth\n`, + ); +} diff --git a/packages/cli-old/src/installers/proofkit-auth.ts b/packages/cli-old/src/installers/proofkit-auth.ts new file mode 100644 index 00000000..89b80532 --- /dev/null +++ b/packages/cli-old/src/installers/proofkit-auth.ts @@ -0,0 +1,220 @@ +import path from "node:path"; +import type { OttoAPIKey } from "@proofkit/fmdapi"; +import chalk from "chalk"; +import dotenv from "dotenv"; +import fs from "fs-extra"; +import ora, { type Ora } from "ora"; +import { type SourceFile, SyntaxKind } from "ts-morph"; +import { getLayouts } from "~/cli/fmdapi.js"; +import * as p from "~/cli/prompts.js"; +import { abortIfCancel, UserAbortedError } from "~/cli/utils.js"; +import { PKG_ROOT } from "~/consts.js"; +import { addConfig, runCodegenCommand } from "~/generators/fmdapi.js"; +import { injectTanstackQuery } from "~/generators/tanstack-query.js"; +import { state } from "~/state.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { getSettings } from "~/utils/parseSettings.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; +import { addToHeaderSlot } from "./auth-shared.js"; +import { installFmAddon } from "./install-fm-addon.js"; +import { installReactEmail } from "./react-email.js"; + +export const proofkitAuthInstaller = async () => { + const spinner = ora("Installing files for auth...").start(); + + const projectDir = state.projectDir; + addPackageDependency({ + projectDir, + dependencies: ["@node-rs/argon2", "@oslojs/binary", "@oslojs/crypto", "@oslojs/encoding", "js-cookie"], + devMode: false, + }); + + addPackageDependency({ + projectDir, + dependencies: ["@types/js-cookie"], + devMode: true, + }); + + // copy all files from template/extras/fmaddon-auth to projectDir/src + await fs.copy(path.join(PKG_ROOT, "template/extras/fmaddon-auth"), path.join(projectDir, "src")); + + const project = getNewProject(projectDir); + + // ensure tanstack query is installed + await injectTanstackQuery({ project }); + + // inject signin/signout components to header slots + addToHeaderSlot( + project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), + "@/components/auth/user-menu", + ); + // addToHeaderSlot( + // project.addSourceFileAtPath( + // path.join( + // projectDir, + // "src/components/AppShell/slot-header-mobile-content.tsx" + // ) + // ), + // "@/components/clerk-auth/user-menu-mobile" + // ); + + addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); + + await addConfig({ + config: { + type: "fmdapi", + envNames: undefined, + clientSuffix: "Layout", + layouts: [ + { + layoutName: "proofkit_auth_sessions", + schemaName: "sessions", + strictNumbers: true, + }, + { + layoutName: "proofkit_auth_users", + schemaName: "users", + strictNumbers: true, + }, + { + layoutName: "proofkit_auth_email_verification", + schemaName: "emailVerification", + strictNumbers: true, + }, + { + layoutName: "proofkit_auth_password_reset", + schemaName: "passwordReset", + strictNumbers: true, + }, + ], + clearOldFiles: true, + validator: false, + path: "./src/server/auth/db", + }, + projectDir, + runCodegen: false, + }); + + // install email files based on the email provider in state + await installReactEmail({ project, installServerFiles: true }); + + protectMainLayout(project.addSourceFileAtPath(path.join(projectDir, "src/app/(main)/layout.tsx"))); + + await formatAndSaveSourceFiles(project); + + let hasProofKitLayouts = false; + while (!hasProofKitLayouts) { + hasProofKitLayouts = await checkForProofKitLayouts(projectDir, spinner); + + if (hasProofKitLayouts) { + spinner.text = "Successfully detected all required layouts in your FileMaker file."; + } else { + const shouldContinue = abortIfCancel( + await p.confirm({ + message: "I have followed the above instructions, continue installing", + initialValue: true, + active: "Continue", + inactive: "Abort", + }), + ); + + if (!shouldContinue) { + throw new UserAbortedError(); + } + } + } + await runCodegenCommand(); + + spinner.succeed("Auth installed successfully"); +}; + +function addToSafeActionClient(sourceFile?: SourceFile) { + if (!sourceFile) { + console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); + return; + } + + sourceFile.addImportDeclaration({ + namedImports: [{ name: "getCurrentSession" }], + moduleSpecifier: "./auth/utils/session", + }); + + // add to end of file + sourceFile.addStatements((writer) => + writer.writeLine(`export const authedActionClient = actionClient.use(async ({ next, ctx }) => { + const { session, user } = await getCurrentSession(); + if (session === null) { + throw new Error("Unauthorized"); + } + + return next({ ctx: { ...ctx, session, user } }); +}); +`), + ); +} + +function protectMainLayout(sourceFile: SourceFile) { + sourceFile.addImportDeclaration({ + defaultImport: "Protect", + moduleSpecifier: "@/components/auth/protect", + }); + + // inject query provider into the root layout + + const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport()); + const bodyElement = exportDefault + ?.getBody() + ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) + ?.getFirstDescendantByKind(SyntaxKind.JsxElement); + + bodyElement?.replaceWithText( + ` + ${bodyElement?.getText()} + `, + ); +} + +async function checkForProofKitLayouts(projectDir: string, spinner: Ora): Promise { + const settings = getSettings(); + + const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === "filemaker"); + + if (!dataSource) { + return false; + } + if (settings.envFile) { + dotenv.config({ + path: path.join(projectDir, settings.envFile), + }); + } + const dataApiKey = process.env[dataSource.envNames.apiKey]; + const fmFile = process.env[dataSource.envNames.database]; + const server = process.env[dataSource.envNames.server]; + + if (!(dataApiKey && fmFile && server)) { + return false; + } + + const existingLayouts = await getLayouts({ + dataApiKey: dataApiKey as OttoAPIKey, + fmFile, + server, + }); + const proofkitAuthLayouts = [ + "proofkit_auth_sessions", + "proofkit_auth_users", + "proofkit_auth_email_verification", + "proofkit_auth_password_reset", + ]; + + const allProofkitAuthLayoutsExist = proofkitAuthLayouts.every((layout) => existingLayouts.some((l) => l === layout)); + + if (allProofkitAuthLayoutsExist) { + return true; + } + + spinner.warn("Required layouts not found"); + await installFmAddon({ addonName: "auth" }); + + return false; +} diff --git a/packages/cli-old/src/installers/proofkit-webviewer.ts b/packages/cli-old/src/installers/proofkit-webviewer.ts new file mode 100644 index 00000000..f3a1a23f --- /dev/null +++ b/packages/cli-old/src/installers/proofkit-webviewer.ts @@ -0,0 +1,84 @@ +import path from "node:path"; +import type { OttoAPIKey } from "@proofkit/fmdapi"; +import chalk from "chalk"; +import dotenv from "dotenv"; +import { getLayouts } from "~/cli/fmdapi.js"; +import * as p from "~/cli/prompts.js"; +import { abortIfCancel, UserAbortedError } from "~/cli/utils.js"; +import { state } from "~/state.js"; +import { getSettings } from "~/utils/parseSettings.js"; +import { installFmAddon } from "./install-fm-addon.js"; + +export async function checkForWebViewerLayouts(): Promise { + const settings = getSettings(); + + const dataSource = settings.dataSources + .filter((s: { type: string }) => s.type === "fm") + .find((s: { name: string; type: string }) => s.name === "filemaker") as + | { + type: "fm"; + name: string; + envNames: { database: string; server: string; apiKey: string }; + } + | undefined; + + if (!dataSource) { + return false; + } + if (settings.envFile) { + dotenv.config({ + path: path.join(state.projectDir, settings.envFile), + }); + } + const dataApiKey = process.env[dataSource.envNames.apiKey] as OttoAPIKey | undefined; + const fmFile = process.env[dataSource.envNames.database]; + const server = process.env[dataSource.envNames.server]; + + if (!(dataApiKey && fmFile && server)) { + return false; + } + + const existingLayouts = await getLayouts({ + dataApiKey, + fmFile, + server, + }); + const webviewerLayouts = ["ProofKitWV"]; + + const allWebViewerLayoutsExist = webviewerLayouts.every((layout) => + existingLayouts.some((l: string) => l === layout), + ); + + if (allWebViewerLayoutsExist) { + console.log( + chalk.green("Successfully detected all required layouts for ProofKit WebViewer in your FileMaker file."), + ); + return true; + } + + await installFmAddon({ addonName: "wv" }); + + return false; +} + +export async function ensureWebViewerAddonInstalled() { + let hasWebViewerLayouts = false; + while (!hasWebViewerLayouts) { + hasWebViewerLayouts = await checkForWebViewerLayouts(); + + if (!hasWebViewerLayouts) { + const shouldContinue = abortIfCancel( + await p.confirm({ + message: "I have followed the above instructions, continue installing", + initialValue: true, + active: "Continue", + inactive: "Abort", + }), + ); + + if (!shouldContinue) { + throw new UserAbortedError(); + } + } + } +} diff --git a/packages/cli-old/src/installers/react-email.ts b/packages/cli-old/src/installers/react-email.ts new file mode 100644 index 00000000..59a90749 --- /dev/null +++ b/packages/cli-old/src/installers/react-email.ts @@ -0,0 +1,211 @@ +import path from "node:path"; +import chalk from "chalk"; +import fs from "fs-extra"; +import type { Project } from "ts-morph"; +import type { PackageJson } from "type-fest"; +import * as p from "~/cli/prompts.js"; + +import { abortIfCancel } from "~/cli/utils.js"; +import { PKG_ROOT } from "~/consts.js"; +import { installDependencies } from "~/helpers/installDependencies.js"; +import { isNonInteractiveMode, state } from "~/state.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { addToEnv } from "~/utils/addToEnvs.js"; +import { logger } from "~/utils/logger.js"; +import { getSettings, setSettings } from "~/utils/parseSettings.js"; +import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; + +export async function installReactEmail({ + ...args +}: { + project?: Project; + noInstall?: boolean; + installServerFiles?: boolean; +}) { + const projectDir = state.projectDir; + + // Exit early if already installed + const settings = getSettings(); + if (settings.ui === "shadcn") { + return false; + } + if (settings.reactEmail) { + return false; + } + + // Ensure emails directory exists + fs.ensureDirSync(path.join(projectDir, "src/emails")); + addPackageDependency({ + dependencies: ["@react-email/components", "@react-email/render"], + devMode: false, + projectDir, + }); + addPackageDependency({ + dependencies: ["react-email", "@react-email/preview-server"], + devMode: true, + projectDir, + }); + + // add a script to package.json + const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as PackageJson; + if (!pkgJson.scripts) { + pkgJson.scripts = {}; + } + pkgJson.scripts["email:preview"] = "email dev --port 3010 --dir=src/emails"; + fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, { + spaces: 2, + }); + + const project = args.project ?? getNewProject(projectDir); + + if (args.installServerFiles) { + const emailProvider = state.emailProvider; + if (emailProvider === "plunk") { + await installPlunk({ project }); + } else if (emailProvider === "resend") { + await installResend({ project }); + } else { + await fs.copy( + path.join(PKG_ROOT, "template/extras/emailProviders/none/email.tsx"), + path.join(projectDir, "src/server/auth/email.tsx"), + ); + } + } + + // Copy base email template(s) into src/emails for preview and reuse + await fs.copy( + path.join(PKG_ROOT, "template/extras/emailTemplates/generic.tsx"), + path.join(projectDir, "src/emails/generic.tsx"), + ); + if (args.installServerFiles) { + await fs.copy( + path.join(PKG_ROOT, "template/extras/emailTemplates/auth-code.tsx"), + path.join(projectDir, "src/emails/auth-code.tsx"), + ); + } + + if (!args.project) { + await formatAndSaveSourceFiles(project); + } + + // Mark as installed + setSettings({ + ...settings, + reactEmail: true, + reactEmailServer: Boolean(args.installServerFiles) || settings.reactEmailServer, + }); + + // Install dependencies unless explicitly skipped + if (!args.noInstall) { + await installDependencies({ projectDir }); + } + return true; +} + +export async function installPlunk({ project }: { project?: Project }) { + const projectDir = state.projectDir; + addPackageDependency({ + dependencies: ["@plunk/node"], + devMode: false, + projectDir, + }); + + let apiKey: string; + if (typeof state.apiKey === "string") { + apiKey = state.apiKey; + } else if (isNonInteractiveMode()) { + apiKey = ""; + } else { + apiKey = abortIfCancel( + await p.text({ + message: `Enter your Plunk API key\n${chalk.dim( + "Enter your Secret API Key from https://app.useplunk.com/settings/api", + )}`, + placeholder: "...or leave blank to do this later", + }), + ); + } + + if (!apiKey) { + logger.warn("You will need to add your Plunk API key to the .env file manually for your app to run."); + } + + console.log(""); + + await addToEnv({ + projectDir, + project, + envs: [ + { + name: "PLUNK_API_KEY", + zodValue: `z.string().startsWith("sk_")`, + type: "server", + defaultValue: apiKey, + }, + ], + }); + + await fs.copy( + path.join(PKG_ROOT, "template/extras/emailProviders/plunk/service.ts"), + path.join(projectDir, "src/server/services/plunk.ts"), + ); + + await fs.copy( + path.join(PKG_ROOT, "template/extras/emailProviders/plunk/email.tsx"), + path.join(projectDir, "src/server/auth/email.tsx"), + ); +} + +export async function installResend({ project }: { project?: Project }) { + const projectDir = state.projectDir; + addPackageDependency({ + dependencies: ["resend"], + devMode: false, + projectDir, + }); + + let apiKey: string; + if (typeof state.apiKey === "string") { + apiKey = state.apiKey; + } else if (isNonInteractiveMode()) { + apiKey = ""; + } else { + apiKey = abortIfCancel( + await p.text({ + message: `Enter your Resend API key\n${chalk.dim( + `Only "Sending Access" permission required: https://resend.com/api-keys`, + )}`, + placeholder: "...or leave blank to do this later", + }), + ); + } + + if (!apiKey) { + logger.warn("You will need to add your Resend API key to the .env file manually for your app to run."); + } + + console.log(""); + + await addToEnv({ + projectDir, + project, + envs: [ + { + name: "RESEND_API_KEY", + zodValue: `z.string().startsWith("re_")`, + type: "server", + defaultValue: apiKey, + }, + ], + }); + + await fs.copy( + path.join(PKG_ROOT, "template/extras/emailProviders/resend/service.ts"), + path.join(projectDir, "src/server/services/resend.ts"), + ); + + await fs.copy( + path.join(PKG_ROOT, "template/extras/emailProviders/resend/email.tsx"), + path.join(projectDir, "src/server/auth/email.tsx"), + ); +} diff --git a/packages/cli-old/src/state.ts b/packages/cli-old/src/state.ts new file mode 100644 index 00000000..58711d4b --- /dev/null +++ b/packages/cli-old/src/state.ts @@ -0,0 +1,33 @@ +import { z } from "zod/v4"; + +const schema = z + .object({ + ci: z.boolean().default(false), + nonInteractive: z.boolean().default(false), + debug: z.boolean().default(false), + localBuild: z.boolean().default(false), + baseCommand: z.enum(["add", "init", "deploy", "upgrade", "remove"]).optional().catch(undefined), + appType: z.enum(["browser", "webviewer"]).optional().catch(undefined), + ui: z.enum(["shadcn", "mantine"]).optional().catch("mantine"), + projectDir: z.string().default(process.cwd()), + authType: z.enum(["clerk", "fmaddon"]).optional(), + emailProvider: z.enum(["plunk", "resend", "none"]).optional(), + dataSource: z.enum(["filemaker", "none"]).optional(), + }) + .passthrough(); + +type ProgramState = z.infer; +export let state: ProgramState = schema.parse({}); + +export function initProgramState(args: unknown) { + const parsed = schema.safeParse(args); + if (parsed.success) { + const mergedState = { ...state, ...parsed.data }; + const nonInteractive = mergedState.nonInteractive || mergedState.ci; + state = { ...mergedState, ci: nonInteractive, nonInteractive }; + } +} + +export function isNonInteractiveMode() { + return state.nonInteractive || state.ci; +} diff --git a/packages/cli-old/src/upgrades/cursorRules.ts b/packages/cli-old/src/upgrades/cursorRules.ts new file mode 100644 index 00000000..1338225f --- /dev/null +++ b/packages/cli-old/src/upgrades/cursorRules.ts @@ -0,0 +1,41 @@ +import path from "node:path"; +import fs from "fs-extra"; + +import { PKG_ROOT } from "~/consts.js"; +import { state } from "~/state.js"; +import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; + +export async function copyCursorRules() { + const projectDir = state.projectDir; + const extrasDir = path.join(PKG_ROOT, "template/extras"); + const cursorRulesSrcDir = path.join(extrasDir, "_cursor/rules"); + const cursorRulesDestDir = path.join(projectDir, ".cursor/rules"); + + if (!fs.existsSync(cursorRulesSrcDir)) { + return; + } + + const pkgManager = getUserPkgManager(); + await fs.ensureDir(cursorRulesDestDir); + await fs.copy(cursorRulesSrcDir, cursorRulesDestDir); + + // Copy package manager specific rules + const conditionalRulesDir = path.join(extrasDir, "_cursor/conditional-rules"); + + const packageManagerRules = { + pnpm: "pnpm.mdc", + npm: "npm.mdc", + yarn: "yarn.mdc", + }; + + const selectedRule = packageManagerRules[pkgManager as keyof typeof packageManagerRules]; + + if (selectedRule) { + const ruleSrc = path.join(conditionalRulesDir, selectedRule); + const ruleDest = path.join(cursorRulesDestDir, "package-manager.mdc"); + + if (fs.existsSync(ruleSrc)) { + await fs.copy(ruleSrc, ruleDest, { overwrite: true }); + } + } +} diff --git a/packages/cli-old/src/upgrades/index.ts b/packages/cli-old/src/upgrades/index.ts new file mode 100644 index 00000000..b72dbc98 --- /dev/null +++ b/packages/cli-old/src/upgrades/index.ts @@ -0,0 +1,69 @@ +import { type appTypes, getSettings, mergeSettings } from "~/utils/parseSettings.js"; +import { copyCursorRules } from "./cursorRules.js"; +import { addShadcn } from "./shadcn.js"; + +interface Upgrade { + key: string; + title: string; + description: string; + appType: (typeof appTypes)[number][]; + function: () => Promise; +} + +const availableUpgrades: Upgrade[] = [ + { + key: "cursorRules", + title: "Upgrade Cursor Rules", + description: "Upgrade the .cursor rules in your project to the latest version.", + appType: ["browser"], + function: copyCursorRules, + }, + { + key: "shadcn", + title: "Add Shadcn", + description: + "Add Shadcn to your project, to support easily adding new components from a variety of component registries.", + appType: ["browser", "webviewer"], + function: addShadcn, + }, +]; + +export type UpgradeKeys = (typeof availableUpgrades)[number]["key"]; + +export function checkForAvailableUpgrades() { + const settings = getSettings(); + if (settings.ui === "shadcn") { + return []; + } + + const appliedUpgrades = settings.appliedUpgrades; + + const neededUpgrades = availableUpgrades.filter( + (upgrade) => !appliedUpgrades.includes(upgrade.key) && upgrade.appType.includes(settings.appType), + ); + + return neededUpgrades.map(({ key, title, description }) => ({ + key, + title, + description, + })); +} + +export async function runAllAvailableUpgrades() { + const upgrades = checkForAvailableUpgrades(); + const settings = getSettings(); + if (settings.ui === "shadcn") { + return; + } + + for (const upgrade of upgrades) { + const upgradeFunction = availableUpgrades.find((u) => u.key === upgrade.key)?.function; + if (upgradeFunction) { + await upgradeFunction(); + const appliedUpgrades = settings.appliedUpgrades; + mergeSettings({ + appliedUpgrades: [...appliedUpgrades, upgrade.key], + }); + } + } +} diff --git a/packages/cli-old/src/upgrades/shadcn.ts b/packages/cli-old/src/upgrades/shadcn.ts new file mode 100644 index 00000000..d385a4d0 --- /dev/null +++ b/packages/cli-old/src/upgrades/shadcn.ts @@ -0,0 +1,53 @@ +import path from "node:path"; +import fs from "fs-extra"; + +import { PKG_ROOT } from "~/consts.js"; +import { installDependencies } from "~/helpers/installDependencies.js"; +import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js"; +import { state } from "~/state.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; + +const BASE_DEPS = [ + "@radix-ui/react-slot", + "@tailwindcss/postcss", + "class-variance-authority", + "clsx", + "lucide-react", + "tailwind-merge", + "tailwindcss", + "tw-animate-css", +] as AvailableDependencies[]; +const BASE_DEV_DEPS = [] as AvailableDependencies[]; + +export async function addShadcn() { + const projectDir = state.projectDir; + + const TEMPLATE_ROOT = path.join(PKG_ROOT, "template/nextjs"); + + // 1. Add dependencies + addPackageDependency({ + dependencies: BASE_DEPS, + devMode: false, + projectDir, + }); + addPackageDependency({ + dependencies: BASE_DEV_DEPS, + devMode: true, + projectDir, + }); + + // 2. Copy config and utility files + fs.copySync(path.join(TEMPLATE_ROOT, "components.json"), path.join(projectDir, "components.json")); + fs.copySync(path.join(TEMPLATE_ROOT, "postcss.config.cjs"), path.join(projectDir, "postcss.config.cjs")); + fs.copySync(path.join(TEMPLATE_ROOT, "src/utils/styles.ts"), path.join(projectDir, "src/utils/styles.ts")); + fs.copySync( + path.join(TEMPLATE_ROOT, "src/config/theme/globals.css"), + path.join(projectDir, "src/config/theme/globals.css"), + ); + + // 3. Install dependencies + await installDependencies(); + + // 4. Success message + console.log("\n✅ shadcn/ui + Tailwind v4 upgrade complete!\n"); +} diff --git a/packages/cli-old/src/utils/addPackageDependency.ts b/packages/cli-old/src/utils/addPackageDependency.ts new file mode 100644 index 00000000..c2d139e7 --- /dev/null +++ b/packages/cli-old/src/utils/addPackageDependency.ts @@ -0,0 +1,32 @@ +import path from "node:path"; +import fs from "fs-extra"; +import sortPackageJson from "sort-package-json"; +import type { PackageJson } from "type-fest"; + +import { type AvailableDependencies, dependencyVersionMap } from "~/installers/dependencyVersionMap.js"; +import { state } from "~/state.js"; + +export const addPackageDependency = (opts: { + dependencies: AvailableDependencies[]; + devMode: boolean; + projectDir?: string; +}) => { + const { dependencies, devMode, projectDir = state.projectDir } = opts; + + const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as PackageJson; + + for (const pkgName of dependencies) { + const version = dependencyVersionMap[pkgName]; + + if (devMode && pkgJson.devDependencies) { + pkgJson.devDependencies[pkgName] = version; + } else if (pkgJson.dependencies) { + pkgJson.dependencies[pkgName] = version; + } + } + const sortedPkgJson = sortPackageJson(pkgJson); + + fs.writeJSONSync(path.join(projectDir, "package.json"), sortedPkgJson, { + spaces: 2, + }); +}; diff --git a/packages/cli-old/src/utils/addToEnvs.ts b/packages/cli-old/src/utils/addToEnvs.ts new file mode 100644 index 00000000..5af7e131 --- /dev/null +++ b/packages/cli-old/src/utils/addToEnvs.ts @@ -0,0 +1,131 @@ +import { execSync } from "node:child_process"; +import path from "node:path"; +import fs from "fs-extra"; +import { type Project, SyntaxKind } from "ts-morph"; + +import { findT3EnvFile } from "~/installers/envVars.js"; +import { state } from "~/state.js"; +import { formatAndSaveSourceFiles, getNewProject } from "./ts-morph.js"; + +interface EnvSchema { + name: string; + zodValue: string; + /** This value will be added to the .env file, unless `addToRuntimeEnv` is set to `false`. */ + defaultValue?: string; + type: "server" | "client"; + addToRuntimeEnv?: boolean; +} + +export async function addToEnv({ + projectDir = state.projectDir, + envs, + envFileDescription, + ...args +}: { + projectDir?: string; + project?: Project; + envs: EnvSchema[]; + envFileDescription?: string; +}) { + const envSchemaFile = findT3EnvFile(); + + const project = args.project ?? getNewProject(projectDir); + const schemaFile = project.addSourceFileAtPath(envSchemaFile); + + if (!schemaFile) { + throw new Error("Schema file not found"); + } + + // Find the createEnv call expression + const createEnvCall = schemaFile + .getDescendantsOfKind(SyntaxKind.CallExpression) + .find((callExpr) => callExpr.getExpression().getText() === "createEnv"); + + if (!createEnvCall) { + throw new Error( + "Could not find createEnv call in schema file. Make sure you have a valid env.ts file with createEnv setup.", + ); + } + + // Get the server object property + const opts = createEnvCall.getArguments()[0]; + if (!opts) { + throw new Error("createEnv call is missing options argument"); + } + + const serverProperty = opts + .getDescendantsOfKind(SyntaxKind.PropertyAssignment) + .find((prop) => prop.getName() === "server") + ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); + + const clientProperty = opts + .getDescendantsOfKind(SyntaxKind.PropertyAssignment) + .find((prop) => prop.getName() === "client") + ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); + + const runtimeEnvProperty = opts + .getDescendantsOfKind(SyntaxKind.PropertyAssignment) + .find((prop) => prop.getName() === "experimental__runtimeEnv") + ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); + + const serverEnvs = envs.filter((env) => env.type === "server"); + const clientEnvs = envs.filter((env) => env.type === "client"); + + for (const env of serverEnvs) { + serverProperty?.addPropertyAssignment({ + name: env.name, + initializer: env.zodValue, + }); + } + + for (const env of clientEnvs) { + clientProperty?.addPropertyAssignment({ + name: env.name, + initializer: env.zodValue, + }); + + runtimeEnvProperty?.addPropertyAssignment({ + name: env.name, + initializer: `process.env.${env.name}`, + }); + } + + const envsString = envs + .filter((env) => env.addToRuntimeEnv ?? true) + .map((env) => `${env.name}=${env.defaultValue ?? ""}`) + .join("\n"); + + const dotEnvFile = path.join(projectDir, ".env"); + + // Only handle .env file if it already exists + if (fs.existsSync(dotEnvFile)) { + const currentFile = fs.readFileSync(dotEnvFile, "utf-8"); + + // Ensure .env is in .gitignore using command line + const gitIgnoreFile = path.join(projectDir, ".gitignore"); + try { + let gitIgnoreContent = ""; + if (fs.existsSync(gitIgnoreFile)) { + gitIgnoreContent = fs.readFileSync(gitIgnoreFile, "utf-8"); + } + + if (!gitIgnoreContent.includes(".env")) { + execSync(`echo ".env" >> "${gitIgnoreFile}"`, { cwd: projectDir }); + } + } catch (_error) { + // Silently ignore gitignore errors + } + + const newContent = `${currentFile} +${envFileDescription ? `# ${envFileDescription}\n${envsString}` : envsString} + `; + + fs.writeFileSync(dotEnvFile, newContent); + } + + if (!args.project) { + await formatAndSaveSourceFiles(project); + } + + return schemaFile; +} diff --git a/packages/cli-old/src/utils/formatting.ts b/packages/cli-old/src/utils/formatting.ts new file mode 100644 index 00000000..8522e45a --- /dev/null +++ b/packages/cli-old/src/utils/formatting.ts @@ -0,0 +1,24 @@ +import { execa } from "execa"; +import type { Project } from "ts-morph"; + +import { state } from "~/state.js"; + +/** + * Formats all source files in a ts-morph Project using biome and saves the changes. + * @param project The ts-morph Project containing the files to format + */ +export async function formatAndSaveSourceFiles(project: Project) { + await project.save(); // save files first + try { + // Run biome format on the project directory + await execa("npx", ["@biomejs/biome", "format", "--write", state.projectDir], { + cwd: state.projectDir, + }); + } catch (error) { + if (state.debug) { + console.log("Error formatting files with biome"); + console.error(error); + } + // Continue even if formatting fails + } +} diff --git a/packages/cli-old/src/utils/getProofKitVersion.ts b/packages/cli-old/src/utils/getProofKitVersion.ts new file mode 100644 index 00000000..496e46a2 --- /dev/null +++ b/packages/cli-old/src/utils/getProofKitVersion.ts @@ -0,0 +1,38 @@ +import path from "node:path"; +import fs from "fs-extra"; +import type { PackageJson } from "type-fest"; + +import { PKG_ROOT } from "~/consts.js"; + +export const getVersion = () => { + const packageJsonPath = path.join(PKG_ROOT, "package.json"); + + const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson; + + return packageJsonContent.version ?? "1.0.0"; +}; + +export const getFmdapiVersion = () => { + return __FMDAPI_VERSION__; +}; + +export const getNodeMajorVersion = () => { + const defaultVersion = "22"; + try { + return process.versions.node.split(".")[0] ?? defaultVersion; + } catch { + return defaultVersion; + } +}; + +export const getProofkitBetterAuthVersion = () => { + return __BETTER_AUTH_VERSION__; +}; + +export const getProofkitWebviewerVersion = () => { + return __WEBVIEWER_VERSION__; +}; + +export const getTypegenVersion = () => { + return __TYPEGEN_VERSION__; +}; diff --git a/packages/cli-old/src/utils/getUserPkgManager.ts b/packages/cli-old/src/utils/getUserPkgManager.ts new file mode 100644 index 00000000..d2e3afdc --- /dev/null +++ b/packages/cli-old/src/utils/getUserPkgManager.ts @@ -0,0 +1,21 @@ +export type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; + +export const getUserPkgManager: () => PackageManager = () => { + // This environment variable is set by npm and yarn but pnpm seems less consistent + const userAgent = process.env.npm_config_user_agent; + + if (userAgent) { + if (userAgent.startsWith("yarn")) { + return "yarn"; + } + if (userAgent.startsWith("pnpm")) { + return "pnpm"; + } + if (userAgent.startsWith("bun")) { + return "bun"; + } + return "npm"; + } + // If no user agent is set, assume pnpm + return "pnpm"; +}; diff --git a/packages/cli-old/src/utils/isTTYError.ts b/packages/cli-old/src/utils/isTTYError.ts new file mode 100644 index 00000000..ccf602ed --- /dev/null +++ b/packages/cli-old/src/utils/isTTYError.ts @@ -0,0 +1 @@ +export class IsTTYError extends Error {} diff --git a/packages/cli-old/src/utils/logger.ts b/packages/cli-old/src/utils/logger.ts new file mode 100644 index 00000000..3ddb9775 --- /dev/null +++ b/packages/cli-old/src/utils/logger.ts @@ -0,0 +1,19 @@ +import chalk from "chalk"; + +export const logger = { + error(...args: unknown[]) { + console.log(chalk.red(...args)); + }, + warn(...args: unknown[]) { + console.log(chalk.yellow(...args)); + }, + info(...args: unknown[]) { + console.log(chalk.cyan(...args)); + }, + success(...args: unknown[]) { + console.log(chalk.green(...args)); + }, + dim(...args: unknown[]) { + console.log(chalk.dim(...args)); + }, +}; diff --git a/packages/cli-old/src/utils/parseNameAndPath.ts b/packages/cli-old/src/utils/parseNameAndPath.ts new file mode 100644 index 00000000..a4d4507e --- /dev/null +++ b/packages/cli-old/src/utils/parseNameAndPath.ts @@ -0,0 +1,42 @@ +import pathModule from "node:path"; + +import { removeTrailingSlash } from "./removeTrailingSlash.js"; + +/** + * Parses the appName and its path from the user input. + * + * Returns a tuple of of `[appName, path]`, where `appName` is the name put in the "package.json" + * file and `path` is the path to the directory where the app will be created. + * + * If `appName` is ".", the name of the directory will be used instead. Handles the case where the + * input includes a scoped package name in which case that is being parsed as the name, but not + * included as the path. + * + * For example: + * + * - dir/@mono/app => ["@mono/app", "dir/app"] + * - dir/app => ["app", "dir/app"] + */ +export const parseNameAndPath = (rawInput: string) => { + const input = removeTrailingSlash(rawInput); + + const paths = input.split("/"); + + let appName = paths.at(-1) ?? ""; + + // If the user ran `npx proofkit .` or similar, the appName should be the current directory + if (appName === ".") { + const parsedCwd = pathModule.resolve(process.cwd()); + appName = pathModule.basename(parsedCwd); + } + + // If the first part is a @, it's a scoped package + const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@")); + if (paths.findIndex((p) => p.startsWith("@")) !== -1) { + appName = paths.slice(indexOfDelimiter).join("/"); + } + + const path = paths.filter((p) => !p.startsWith("@")).join("/"); + + return [appName, path] as const; +}; diff --git a/packages/cli-old/src/utils/parseSettings.ts b/packages/cli-old/src/utils/parseSettings.ts new file mode 100644 index 00000000..eb77a8ec --- /dev/null +++ b/packages/cli-old/src/utils/parseSettings.ts @@ -0,0 +1,153 @@ +import path from "node:path"; +import fs from "fs-extra"; +import { z } from "zod/v4"; + +import { state } from "~/state.js"; + +const authSchema = z + .discriminatedUnion("type", [ + z.object({ + type: z.literal("clerk"), + }), + z.object({ + type: z.literal("next-auth"), + }), + z.object({ + type: z.literal("proofkit").transform(() => "fmaddon"), + }), + z.object({ + type: z.literal("fmaddon"), + }), + z.object({ + type: z.literal("better-auth"), + }), + z.object({ + type: z.literal("none"), + }), + ]) + .default({ type: "none" }); + +export const envNamesSchema = z.object({ + database: z.string().default("FM_DATABASE"), + server: z.string().default("FM_SERVER"), + apiKey: z.string().default("OTTO_API_KEY"), +}); +export const dataSourceSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("fm"), + name: z.string(), + envNames: envNamesSchema, + }), + z.object({ + type: z.literal("supabase"), + name: z.string(), + }), +]); +export type DataSource = z.infer; + +export const appTypes = ["browser", "webviewer"] as const; + +export const uiTypes = ["shadcn", "mantine"] as const; +export type Ui = (typeof uiTypes)[number]; + +const settingsSchema = z.discriminatedUnion("ui", [ + z.object({ + ui: z.literal("mantine"), + appType: z.enum(appTypes).default("browser"), + auth: authSchema, + envFile: z.string().optional(), + dataSources: z.array(dataSourceSchema).default([]), + tanstackQuery: z.boolean().catch(false), + replacedMainPage: z.boolean().catch(false), + // Whether React Email scaffolding has been installed + reactEmail: z.boolean().catch(false), + // Whether provider-specific server email sender files have been installed + reactEmailServer: z.boolean().catch(false), + appliedUpgrades: z.array(z.string()).default([]), + registryUrl: z.url().optional(), + registryTemplates: z.array(z.string()).default([]), + }), + z.object({ + ui: z.literal("shadcn"), + appType: z.enum(appTypes).default("browser"), + envFile: z.string().optional(), + dataSources: z.array(dataSourceSchema).default([]), + replacedMainPage: z.boolean().catch(false), + registryUrl: z.url().optional(), + registryTemplates: z.array(z.string()).default([]), + }), +]); + +export const defaultSettings = settingsSchema.parse({ + auth: { type: "none" }, + ui: "shadcn", + appType: "browser", + dataSources: [], + replacedMainPage: false, + registryTemplates: [], +}); + +let settings: Settings | undefined; +export const getSettings = () => { + if (settings) { + return settings; + } + + const settingsPath = path.join(state.projectDir, "proofkit.json"); + + // Check if the settings file exists before trying to read it + if (!fs.existsSync(settingsPath)) { + throw new Error(`ProofKit settings file not found at: ${settingsPath}`); + } + + let settingsFile: unknown = fs.readJSONSync(settingsPath); + + if (typeof settingsFile === "object" && settingsFile !== null && !("ui" in settingsFile)) { + settingsFile = { ...settingsFile, ui: "mantine" }; + } + + const parsed = settingsSchema.parse(settingsFile); + + state.appType = parsed.appType; + return parsed; +}; + +export type Settings = z.infer; + +export function mergeSettings(_settings: Partial) { + const settings = getSettings(); + const merged = { ...settings, ..._settings }; + const validated = settingsSchema.parse(merged); + setSettings(validated); +} + +export function setSettings(_settings: Settings) { + fs.writeJSONSync(path.join(state.projectDir, "proofkit.json"), _settings, { + spaces: 2, + }); + settings = _settings; + return settings; +} + +/** + * Validates and sets the envFile in settings only if the file exists. + * Used during stealth initialization to avoid setting non-existent env files. + */ +export function validateAndSetEnvFile(envFileName = ".env") { + const settings = getSettings(); + const envFilePath = path.join(state.projectDir, envFileName); + + if (fs.existsSync(envFilePath)) { + const updatedSettings = { ...settings, envFile: envFileName }; + setSettings(updatedSettings); + return envFileName; + } + + // If no env file exists, ensure envFile is undefined in settings + if (settings.envFile) { + const { envFile, ...settingsWithoutEnvFile } = settings; + setSettings(settingsWithoutEnvFile as Settings); + } + + return undefined; +} diff --git a/packages/cli-old/src/utils/proofkitReleaseChannel.ts b/packages/cli-old/src/utils/proofkitReleaseChannel.ts new file mode 100644 index 00000000..aa7ecf17 --- /dev/null +++ b/packages/cli-old/src/utils/proofkitReleaseChannel.ts @@ -0,0 +1,93 @@ +import path from "node:path"; +import fs from "fs-extra"; +import semver from "semver"; + +import { + getFmdapiVersion, + getProofkitBetterAuthVersion, + getProofkitWebviewerVersion, + getTypegenVersion, + getVersion, +} from "~/utils/getProofKitVersion.js"; + +export type ProofkitReleaseTag = "latest" | "beta"; + +interface ChangesetPreState { + mode?: string; + tag?: string; +} + +function findRepoRootWithChangeset(startDir: string): string | null { + let currentDir = path.resolve(startDir); + const { root } = path.parse(currentDir); + + while (currentDir !== root) { + if (fs.existsSync(path.join(currentDir, ".changeset"))) { + return currentDir; + } + currentDir = path.dirname(currentDir); + } + + return null; +} + +function readChangesetPreState(startDir = process.cwd()): ChangesetPreState | null { + const repoRoot = findRepoRootWithChangeset(startDir); + if (!repoRoot) { + return null; + } + + const prePath = path.join(repoRoot, ".changeset", "pre.json"); + if (!fs.existsSync(prePath)) { + return null; + } + + try { + return fs.readJSONSync(prePath) as ChangesetPreState; + } catch { + return null; + } +} + +export function hasAnyPrereleaseVersion(versionCandidates?: Array) { + if (versionCandidates) { + return versionCandidates.some((version) => { + if (!version) { + return false; + } + return semver.valid(version) && semver.prerelease(version); + }); + } + + const readVersion = (getter: () => string) => { + try { + return getter(); + } catch { + return null; + } + }; + + const proofkitVersions = [ + readVersion(getVersion), + readVersion(getFmdapiVersion), + readVersion(getProofkitWebviewerVersion), + readVersion(getTypegenVersion), + readVersion(getProofkitBetterAuthVersion), + ].filter((version): version is string => Boolean(version)); + + return proofkitVersions.some((version) => semver.valid(version) && semver.prerelease(version)); +} + +export function getProofkitReleaseTag(startDir = process.cwd()): ProofkitReleaseTag { + const preState = readChangesetPreState(startDir); + + if (preState?.mode === "pre" && preState.tag === "beta") { + return "beta"; + } + + if (hasAnyPrereleaseVersion()) { + return "beta"; + } + + return "latest"; +} diff --git a/packages/cli-old/src/utils/removeTrailingSlash.ts b/packages/cli-old/src/utils/removeTrailingSlash.ts new file mode 100644 index 00000000..051c3322 --- /dev/null +++ b/packages/cli-old/src/utils/removeTrailingSlash.ts @@ -0,0 +1,6 @@ +export const removeTrailingSlash = (input: string) => { + if (input.length > 1 && input.endsWith("/")) { + return input.slice(0, -1); + } + return input; +}; diff --git a/packages/cli-old/src/utils/renderTitle.ts b/packages/cli-old/src/utils/renderTitle.ts new file mode 100644 index 00000000..d0f89738 --- /dev/null +++ b/packages/cli-old/src/utils/renderTitle.ts @@ -0,0 +1,20 @@ +import gradient from "gradient-string"; + +import { TITLE_TEXT } from "~/consts.js"; +import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; + +const proofTheme = { + purple: "#89216B", + lightPurple: "#D15ABB", + orange: "#FF595E", +}; + +export const proofGradient = gradient(Object.values(proofTheme)); +export const renderTitle = () => { + // resolves weird behavior where the ascii is offset + const pkgManager = getUserPkgManager(); + if (pkgManager === "yarn" || pkgManager === "pnpm") { + console.log(""); + } + console.log(proofGradient.multiline(TITLE_TEXT)); +}; diff --git a/packages/cli-old/src/utils/renderVersionWarning.ts b/packages/cli-old/src/utils/renderVersionWarning.ts new file mode 100644 index 00000000..fd046831 --- /dev/null +++ b/packages/cli-old/src/utils/renderVersionWarning.ts @@ -0,0 +1,86 @@ +import { execSync } from "node:child_process"; +import https from "node:https"; +import chalk from "chalk"; +import * as semver from "semver"; +import * as p from "~/cli/prompts.js"; + +import { cliName, npmName } from "~/consts.js"; +import { getVersion } from "./getProofKitVersion.js"; +import { getUserPkgManager } from "./getUserPkgManager.js"; +import { logger } from "./logger.js"; + +export const renderVersionWarning = (npmVersion: string) => { + const currentVersion = getVersion(); + + // Check if current version is a pre-release (beta, alpha, etc.) + if (semver.prerelease(currentVersion)) { + logger.warn(` You are using a pre-release version of ${cliName}.`); + logger.warn(" Please report any bugs you encounter."); + } else if (semver.valid(currentVersion) && semver.valid(npmVersion) && semver.lt(currentVersion, npmVersion)) { + logger.warn(` You are using an outdated version of ${cliName}.`); + logger.warn(" Your version:", `${currentVersion}.`, "Latest version in the npm registry:", npmVersion); + logger.warn(" Please run the CLI with @latest to get the latest updates."); + } + console.log(""); +}; + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + * https://github.com/facebook/create-react-app/blob/main/packages/create-react-app/LICENSE + */ +interface DistTagsBody { + latest: string; +} + +function checkForLatestVersion(): Promise { + return new Promise((resolve, reject) => { + https + .get("https://registry.npmjs.org/-/package/@proofkit/cli/dist-tags", (res) => { + if (res.statusCode === 200) { + let body = ""; + res.on("data", (data) => { + body += data; + }); + res.on("end", () => { + resolve((JSON.parse(body) as DistTagsBody).latest); + }); + } else { + reject(); + } + }) + .on("error", () => { + // logger.error("Unable to check for latest version."); + reject(); + }); + }); +} + +export const getNpmVersion = async () => + // `fetch` to the registry is faster than `npm view` so we try that first + checkForLatestVersion().catch(() => { + try { + return execSync("npm view proofkit version").toString().trim(); + } catch { + return null; + } + }); + +export const checkAndRenderVersionWarning = async () => { + const npmVersion = await getNpmVersion(); + const currentVersion = getVersion(); + + // Only show warning if current version is valid, npm version is valid, and current is actually older + if (npmVersion && semver.valid(currentVersion) && semver.valid(npmVersion) && semver.lt(currentVersion, npmVersion)) { + const pkgManager = getUserPkgManager(); + p.log.warn( + `${chalk.yellow( + `You are using an outdated version of ${cliName}.`, + )} Your version: ${currentVersion}. Latest version: ${npmVersion}. + Run ${chalk.magenta.bold(`${pkgManager} install ${npmName}@latest`)} to get the latest updates.`, + ); + } + return { npmVersion, currentVersion }; +}; diff --git a/packages/cli-old/src/utils/ts-morph.ts b/packages/cli-old/src/utils/ts-morph.ts new file mode 100644 index 00000000..92d58954 --- /dev/null +++ b/packages/cli-old/src/utils/ts-morph.ts @@ -0,0 +1,25 @@ +import path from "node:path"; +import { Project, type ReturnStatement, SyntaxKind } from "ts-morph"; + +export { formatAndSaveSourceFiles } from "./formatting.js"; + +export function ensureReturnStatementIsWrappedInFragment(returnStatement: ReturnStatement | undefined) { + const expression = + returnStatement?.getExpressionIfKind(SyntaxKind.ParenthesizedExpression)?.getExpression() ?? + returnStatement?.getExpression(); + + if (expression?.isKind(SyntaxKind.JsxFragment)) { + return returnStatement; + } + + returnStatement?.replaceWithText(`return <>${expression};`); + return returnStatement; +} + +export function getNewProject(projectDir?: string) { + const project = new Project({ + tsConfigFilePath: path.join(projectDir ?? process.cwd(), "tsconfig.json"), + }); + + return project; +} diff --git a/packages/cli-old/src/utils/validateAppName.ts b/packages/cli-old/src/utils/validateAppName.ts new file mode 100644 index 00000000..b5b4e42e --- /dev/null +++ b/packages/cli-old/src/utils/validateAppName.ts @@ -0,0 +1,22 @@ +import { removeTrailingSlash } from "./removeTrailingSlash.js"; + +const validationRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; + +//Validate a string against allowed package.json names +export const validateAppName = (rawInput: string) => { + const input = removeTrailingSlash(rawInput); + const paths = input.split("/"); + + // If the first part is a @, it's a scoped package + const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@")); + + let appName = paths.at(-1); + if (paths.findIndex((p) => p.startsWith("@")) !== -1) { + appName = paths.slice(indexOfDelimiter).join("/"); + } + + if (input === "." || validationRegExp.test(appName ?? "")) { + return; + } + return "Name must consist of only lowercase alphanumeric characters, '-', and '_'"; +}; diff --git a/packages/cli-old/src/utils/validateImportAlias.ts b/packages/cli-old/src/utils/validateImportAlias.ts new file mode 100644 index 00000000..bd33ca61 --- /dev/null +++ b/packages/cli-old/src/utils/validateImportAlias.ts @@ -0,0 +1,6 @@ +export const validateImportAlias = (input: string) => { + if (input.startsWith(".") || input.startsWith("/")) { + return "Import alias can't start with '.' or '/'"; + } + return; +}; diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc new file mode 100644 index 00000000..5ce7a9e0 --- /dev/null +++ b/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc @@ -0,0 +1,51 @@ +--- +description: +globs: +alwaysApply: true +--- +# Next.js Framework Configuration + +This rule documents the Next.js framework setup and conventions used in this project. + + +name: nextjs_framework +description: Documents Next.js framework setup, routing conventions, and best practices +filters: + - type: file_extension + pattern: "\\.(ts|tsx)$" + - type: directory + pattern: "src/(app|components)/" + +conventions: + routing: + - App Router is used (not Pages Router) + - Routes are defined in src/app directory + - Layout components should be named layout.tsx + - Page components should be named page.tsx + - Loading states should be in loading.tsx + - Error boundaries should be in error.tsx + + components: + - React Server Components (RSC) are default + - Client components must be marked with "use client" + - Components live in src/components/ + - Shared layouts in src/components/layouts/ + - UI components in src/components/ui/ + + data_fetching: + - Server components fetch data directly + - Client components use React Query + - API routes defined in src/app/api/ + - Server actions used for mutations + +frameworks: + next: "15.1.7" + react: "19.0.0-rc" + typescript: "^5" + mantine: "^7.17.0" + tanstack_query: "^5.59.0" + +metadata: + priority: high + version: 1.0 + \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc new file mode 100644 index 00000000..3b030fa5 --- /dev/null +++ b/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc @@ -0,0 +1,60 @@ +--- +description: | + This rule documents the package manager configuration and usage. It should be included when: + 1. Installing dependencies + 2. Running scripts + 3. Managing project packages + 4. Running development commands + 5. Executing build or test operations +globs: + - "package.json" + - "package-lock.json" + - ".npmrc" +alwaysApply: true +--- +# Package Manager Configuration + +This rule documents the package manager setup and usage requirements. + + +name: package_manager +description: Documents package manager configuration and usage requirements + +configuration: + name: "npm" + version: "latest" + commands: + install: "npm install" + build: "npm run build" + dev: "npm run dev" + typegen: "npm run typegen" + typecheck: "npm run tsc" + notes: "Always use npm instead of yarn or pnpm for consistency" + dev_server_guidelines: + - "Never relaunch the dev server command if it may already be running" + - "Use npm run dev only when explicitly needed to start the server for the first time" + - "For code changes, just save the files and the server will automatically reload" + +examples: + - description: "Installing dependencies" + correct: "npm install" + incorrect: + - "pnpm install" + - "yarn install" + + - description: "Running scripts" + correct: "npm run script-name" + incorrect: + - "pnpm run script-name" + - "yarn script-name" + + - description: "Adding dependencies" + correct: "npm install package-name" + incorrect: + - "pnpm add package-name" + - "yarn add package-name" + +metadata: + priority: high + version: 1.0 + \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc new file mode 100644 index 00000000..d25da047 --- /dev/null +++ b/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc @@ -0,0 +1,65 @@ +--- +description: | +globs: +alwaysApply: true +--- +--- +description: | + This rule documents the package manager configuration and usage. It should be included when: + 1. Installing dependencies + 2. Running scripts + 3. Managing project packages + 4. Running development commands + 5. Executing build or test operations +globs: + - "package.json" + - "pnpm-lock.yaml" + - ".npmrc" +alwaysApply: true +--- +# Package Manager Configuration + +This rule documents the package manager setup and usage requirements. + + +name: package_manager +description: Documents package manager configuration and usage requirements + +configuration: + name: "pnpm" + version: "latest" + commands: + install: "pnpm install" + build: "pnpm build" + dev: "pnpm dev" + typegen: "pnpm typegen" + typecheck: "pnpm tsc" + notes: "Always use pnpm instead of npm or yarn for consistency" + dev_server_guidelines: + - "Never relaunch the dev server command if it may already be running" + - "Use pnpm dev only when explicitly needed to start the server for the first time" + - "For code changes, just save the files and the server will automatically reload" + +examples: + - description: "Installing dependencies" + correct: "pnpm install" + incorrect: + - "npm install" + - "yarn install" + + - description: "Running scripts" + correct: "pnpm run script-name" + incorrect: + - "npm run script-name" + - "yarn script-name" + + - description: "Adding dependencies" + correct: "pnpm add package-name" + incorrect: + - "npm install package-name" + - "yarn add package-name" + +metadata: + priority: high + version: 1.0 + \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc new file mode 100644 index 00000000..5672e80e --- /dev/null +++ b/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc @@ -0,0 +1,60 @@ +--- +description: | + This rule documents the package manager configuration and usage. It should be included when: + 1. Installing dependencies + 2. Running scripts + 3. Managing project packages + 4. Running development commands + 5. Executing build or test operations +globs: + - "package.json" + - "yarn.lock" + - ".yarnrc" +alwaysApply: true +--- +# Package Manager Configuration + +This rule documents the package manager setup and usage requirements. + + +name: package_manager +description: Documents package manager configuration and usage requirements + +configuration: + name: "yarn" + version: "latest" + commands: + install: "yarn install" + build: "yarn build" + dev: "yarn dev" + typegen: "yarn typegen" + typecheck: "yarn tsc" + notes: "Always use yarn instead of npm or pnpm for consistency" + dev_server_guidelines: + - "Never relaunch the dev server command if it may already be running" + - "Use yarn dev only when explicitly needed to start the server for the first time" + - "For code changes, just save the files and the server will automatically reload" + +examples: + - description: "Installing dependencies" + correct: "yarn install" + incorrect: + - "npm install" + - "pnpm install" + + - description: "Running scripts" + correct: "yarn script-name" + incorrect: + - "npm run script-name" + - "pnpm run script-name" + + - description: "Adding dependencies" + correct: "yarn add package-name" + incorrect: + - "npm install package-name" + - "pnpm add package-name" + +metadata: + priority: high + version: 1.0 + \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc b/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc new file mode 100644 index 00000000..061da499 --- /dev/null +++ b/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc @@ -0,0 +1,88 @@ +--- +description: | + This rule documents how to manage and organize Cursor rules. It should be included when: + 1. Creating or modifying Cursor rules + 2. Organizing documentation for the codebase + 3. Setting up new development patterns + 4. Adding project-wide conventions + 5. Managing rule file locations + 6. Updating rule descriptions or globs + 7. Working with .cursor directory structure +globs: + - ".cursor/rules/*.mdc" + - ".cursor/config/*.json" + - ".cursor/settings/*.json" +alwaysApply: true +--- +# Cursor Rules Location + +Rules for placing and organizing Cursor rule files in the repository. + + +name: cursor_rules_location +description: Standards for placing Cursor rule files in the correct directory +filters: + # Match any .mdc files + - type: file_extension + pattern: "\\.mdc$" + # Match files that look like Cursor rules + - type: content + pattern: "(?s).*?" + # Match file creation events + - type: event + pattern: "file_create" + +actions: + - type: reject + conditions: + - pattern: "^(?!\\.\\/\\.cursor\\/rules\\/.*\\.mdc$)" + message: "Cursor rule files (.mdc) must be placed in the .cursor/rules directory" + + - type: suggest + message: | + When creating Cursor rules: + + 1. Always place rule files in PROJECT_ROOT/.cursor/rules/: + ``` + .cursor/rules/ + ├── your-rule-name.mdc + ├── another-rule.mdc + └── ... + ``` + + 2. Follow the naming convention: + - Use kebab-case for filenames + - Always use .mdc extension + - Make names descriptive of the rule's purpose + + 3. Directory structure: + ``` + PROJECT_ROOT/ + ├── .cursor/ + │ └── rules/ + │ ├── your-rule-name.mdc + │ └── ... + └── ... + ``` + + 4. Never place rule files: + - In the project root + - In subdirectories outside .cursor/rules + - In any other location + - Inside of the cursor-rules.mdc file + +examples: + - input: | + # Bad: Rule file in wrong location + rules/my-rule.mdc + my-rule.mdc + .rules/my-rule.mdc + + # Good: Rule file in correct location + .cursor/rules/my-rule.mdc + output: "Correctly placed Cursor rule file" + +metadata: + priority: high + version: 1.0 + \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc b/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc new file mode 100644 index 00000000..dd6d6716 --- /dev/null +++ b/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc @@ -0,0 +1,176 @@ +--- +description: | + This rule provides guidance for working with the FileMaker Data API in this project. It should be included when: + 1. Working with database operations or data fetching + 2. Encountering database-related errors or type issues + 3. Making changes to FileMaker schemas or layouts + 4. Implementing new data access patterns + 5. Discussing alternative data storage solutions + 6. Working with server-side API routes or actions +globs: + - "src/**/*.ts" + - "src/**/*.tsx" + - "**/fmschema.config.mjs" + - "src/**/actions/*.ts" +alwaysApply: true +--- +# FileMaker Data API Integration + +This rule documents how the FileMaker Data API is integrated and used in the project. + + +name: filemaker_api +description: Documents FileMaker Data API integration patterns and conventions. FileMaker is the ONLY data source for this application - no SQL or other databases should be used. +filters: + - type: file_extension + pattern: "\\.(ts|tsx)$" + - type: directory + pattern: "src/server/" + - type: content + pattern: "(@proofkit/cli|ZodError|typegen)" + +data_source_policy: + exclusive_source: "FileMaker Data API" + prohibited: + - "SQL databases" + - "NoSQL databases" + - "Local storage for persistent data" + - "Direct file system storage" + reason: "All data operations must go through FileMaker to maintain data integrity and business logic" + +troubleshooting: + priority_order: + - "ALWAYS run `{package-manager} typegen` first for ANY data loading issues" + - "DO NOT check environment variables unless you have a specific error message pointing to them" + - "Check for FileMaker schema changes" + - "Verify type definitions match current schema" + - "Review Zod validation errors" + rationale: "Most data loading issues are resolved by running typegen. Environment variables are rarely the cause of data loading problems and should not be investigated unless specific error messages indicate an authentication or connection issue." + +conventions: + api_setup: + - Uses @proofkit/fmdapi package version ^5.0.0 + - Configuration in fmschema.config.mjs + - Environment variables in .env for connection details + - Type generation via `{package-manager} typegen` command + + data_access: + - ALL data operations MUST use FileMaker Data API + - Server-side only API calls via @proofkit/fmdapi + - Type-safe database operations + - Centralized error handling + - Connection pooling and session management + - No direct database connections outside FileMaker + + data_operations: + create: + - Use layout.create({ fieldData: {...} }) + - Validate input against Zod schemas + - Returns recordId of created record + - Handle duplicates via FileMaker business logic + read: + - Use layout.get({ recordId }) for single record by ID + - Use layout.find({ query, limit, offset, sort }) for multiple records + - Use layout.maybeFindFirst({ query }) for optional single record + - Support for complex queries and sorting + update: + - Use layout.update({ recordId, fieldData }) + - Follow FileMaker field naming conventions + - Respect FileMaker validation rules + delete: + - Use layout.delete({ recordId }) + - Respect FileMaker deletion rules + - Handle cascading deletes via FileMaker + query_options: + - Limit and offset for pagination + - Sort by multiple fields with ascend/descend + - Complex query criteria with operators (==, *, etc.) + - Optional type-safe responses with Zod validation + + security: + - Credentials stored in environment variables + - No direct client-side FM API access + - API routes validate authentication + - Data sanitization before queries + - All database access through FileMaker only + +type_generation: + process: + - "IMPORTANT: Running `{package-manager} typegen` solves almost all data loading problems" + - "Run `{package-manager} typegen` after any FileMaker schema changes" + - "Run `{package-manager} typegen` as first step when troubleshooting data issues" + - "Types are generated from FileMaker database schema" + - "Generated types are used in server actions and components" + - "Zod schemas validate runtime data against types" + + common_issues: + schema_changes: + symptoms: + - "No data appearing in tables" + - "ZodError during runtime" + - "Missing or renamed fields" + - "Type mismatches in responses" + - "Empty query results" + solution: "ALWAYS run `{package-manager} typegen && {package-manager} tsc` first" + important_note: "Do NOT check environment variables as a cause for data loading problems unless you have a specific known error that points to environment variables. Most data loading issues are resolved by running typegen." + + field_types: + symptoms: + - "Unexpected null values" + - "Type conversion errors" + - "Invalid date formats" + solution: "Update Zod schemas and type definitions" + + security_notes: + - "Never display, log, or commit environment variables" + - "Never check environment variable values directly" + - "Keep .env files out of version control" + - "When troubleshooting, only verify if variables exist, never their values" + +patterns: + - Server actions wrap FM API calls + - Type definitions generated from FM schema + - Error boundaries for FM API errors + - Rate limiting on API routes + - Caching strategies for frequent queries + +dependencies: + fmdapi: "@proofkit/fmdapi@^5.0.0" + proofkit: "@proofkit/cli@^1.0.0" + +keywords: + database: + - "FileMaker" + - "FMREST" + - "Database schema" + - "Field types" + - "Type generation" + - "Schema changes" + - "Exclusive data source" + - "No SQL" + - "FileMaker only" + - "Data API" + errors: + - "ZodError" + - "TypeError" + - "ValidationError" + - "Missing field" + - "Runtime error" + commands: + - "typegen" + - "tsc" + - "type checking" + - "schema update" + operations: + - "FM.create" + - "FM.find" + - "FM.get" + - "FM.update" + - "FM.delete" + - "FileMaker layout" + - "FileMaker query" + +metadata: + priority: high + version: 1.0 + \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc b/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc new file mode 100644 index 00000000..797fd3cc --- /dev/null +++ b/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc @@ -0,0 +1,240 @@ +--- +description: | +globs: +alwaysApply: false +--- +# Troubleshooting and Maintenance Patterns + +This rule documents common issues, error patterns, and their solutions in the project. + + +name: troubleshooting_patterns +description: Documents common runtime errors, type errors, and solutions. All data operations MUST use FileMaker Data API exclusively. +filters: + - type: file_extension + pattern: "\\.(ts|tsx)$" + - type: content + pattern: "(Error|error|ZodError|TypeError|ValidationError|@proofkit/fmdapi)" + +initial_debugging_steps: + priority: "ALWAYS run `{package-manager} typegen` first for any data-related issues" + steps: + - "Run `{package-manager} typegen` to ensure types match FileMaker schema" + - "Check if error persists after typegen" + - "If error persists, check console for exact error messages" + - "Look for patterns in the troubleshooting guide below" + common_console_errors: + zod_errors: + pattern: "ZodError: [path] invalid_type..." + likely_cause: "Field name mismatch or missing field" + example: "ZodError: nameFirst expected string, got undefined" + solution: "Run typegen first, then check field names in FileMaker schema" + type_errors: + pattern: "TypeError: Cannot read property 'X' of undefined" + likely_cause: "Accessing field before data is loaded or field name mismatch" + solution: "Run typegen first, then add null checks or loading states" + network_errors: + pattern: "Failed to fetch" or "Network error" + likely_cause: "FileMaker connection issues" + solution: "Run typegen first, then check FileMaker server status and credentials" + +data_source_validation: + requirement: "All data operations must use FileMaker Data API exclusively" + first_step_for_data_issues: "ALWAYS run `{package-manager} typegen` first" + common_mistakes: + - "Attempting to use SQL queries" + - "Adding direct database connections" + - "Using local storage for persistent data" + - "Implementing alternative data stores" + - "Skipping typegen after FileMaker schema changes" + - "Using incorrect field names from old schema" + correct_approach: + - "Run typegen first" + - "Use @proofkit/fmdapi for all data operations" + - "Follow FileMaker layout and field conventions" + - "Use layout.create, layout.find, layout.get, layout.update, layout.delete" + - "Use layout.maybeFindFirst for optional records" + +error_patterns: + field_name_mismatches: + symptoms: + - "ZodError: invalid_type at path [fieldName]" + - "Property 'X' does not exist on type 'Y'" + - "TypeScript errors about missing properties" + common_examples: + - "nameFirst vs firstName" + - "lastName vs nameLast" + - "postalCode vs postal_code" + - "phoneNumber vs phone" + cause: "Mismatch between component field names and FileMaker schema" + solution: + steps: + - "Run `{package-manager} typegen` to update types" + - "Look at generated types in src/config/schemas/filemaker/" + - "Update component field names to match schema" + - "Check console for exact field name in error" + files_to_check: + - "src/config/schemas/filemaker/*.ts" + - "Component files using the fields" + + zod_validation_errors: + symptoms: + - "Runtime ZodError: invalid_type" + - "Zod schema validation failed" + - "Property not found in schema" + - "Unexpected field in response" + cause: "FileMaker database schema changes not reflected in TypeScript types" + solution: + steps: + - "Run `{package-manager} typegen` to regenerate types from FileMaker schema" + - "Run `{package-manager} tsc` to identify type mismatches" + - "Check console for exact error message" + - "Update affected components and server actions" + commands: + - "{package-manager} typegen" + - "{package-manager} tsc" + files_to_check: + - "src/server/actions/*" + - "src/server/schema/*" + - "fmschema.config.mjs" + + filemaker_connection: + symptoms: + - "ETIMEDOUT connecting to FileMaker" + - "Invalid FileMaker credentials" + - "Session token expired" + - "Layout not found" + - "Field not found in layout" + - "Invalid find criteria" + - "No data appearing or queries returning empty" + cause: "FileMaker connection, authentication, or query issues" + solution: + steps: + - "Run `{package-manager} typegen` to ensure schema is up to date" + - "Check FileMaker Server status" + - "Validate credentials and permissions" + - "Note: As an AI, you cannot directly check environment variables - always ask the user to verify them if this is determined to be the issue" + - "Verify layout names and field access" + - "Check FileMaker query syntax" + files_to_check: + - "src/server/lib/fm.ts" + - "fmschema.config.mjs" + + data_access_errors: + symptoms: + - "Invalid operation on FileMaker record" + - "Record not found" + - "Insufficient permissions" + - "Invalid find request" + cause: "Incorrect FileMaker Data API usage or permissions" + solution: + steps: + - "Run `{package-manager} typegen` to ensure schema is up to date" + - "Verify FileMaker layout privileges" + - "Check record existence before operations" + - "Validate find criteria format" + - "Use proper FM API methods" + files_to_check: + - "src/server/actions/*" + - "src/server/lib/fm.ts" + + type_errors: + symptoms: + - "Type ... is not assignable to type ..." + - "Property ... does not exist on type ..." + - "Argument of type ... is not assignable" + cause: "Mismatch between FileMaker schema and TypeScript types" + solution: + steps: + - "Run `{package-manager} typegen` to regenerate types" + - "Run `{package-manager} tsc` to identify type mismatches" + - "Update type definitions if needed" + - "Check for null/undefined handling" + commands: + - "{package-manager} typegen && {package-manager} tsc" + + data_sync_issues: + symptoms: + - "Missing fields in table" + - "Unexpected null values" + - "Fields showing as blank" + - "Type mismatches between FM and frontend" + first_step: "ALWAYS run `{package-manager} typegen` first" + cause: "Mismatch between FileMaker schema and TypeScript types, or outdated type definitions" + solution: + steps: + - "Run `{package-manager} typegen` to regenerate types from FileMaker schema" + - "Check for any type errors in the console" + - "Verify field names match exactly between FM and generated types" + - "Update components if field names have changed" + commands: + - "{package-manager} typegen" + - "{package-manager} tsc" + files_to_check: + - "src/config/schemas/filemaker/*.ts" + - "fmschema.config.mjs" + +maintenance_tasks: + schema_sync: + description: "Keep FileMaker schema and TypeScript types in sync" + frequency: "After any FileMaker schema changes" + steps: + - "Run typegen to update types" + - "Run TypeScript compiler" + - "Update affected components" + impact: "Prevents runtime errors and type mismatches" + + type_checking: + description: "Regular type checking for early error detection" + frequency: "Before deployments and after schema changes" + commands: + - "{package-manager} tsc --noEmit" + impact: "Catches type errors before runtime" + +keywords: + errors: + - "ZodError" + - "TypeError" + - "ValidationError" + - "Schema mismatch" + - "Type mismatch" + - "Runtime error" + - "Database schema" + - "Type generation" + - "FileMaker fields" + - "Missing property" + - "Invalid type" + - "Layout not found" + - "Field not found" + - "Invalid find request" + solutions: + - "typegen" + - "tsc" + - "type checking" + - "schema update" + - "validation fix" + - "error handling" + - "FM API methods" + - "FileMaker layout" + operations: + - "layout.create" + - "layout.find" + - "layout.get" + - "layout.update" + - "layout.delete" + - "layout.maybeFindFirst" + - "recordId" + - "fieldData" + - "query parameters" + - "sort options" + data_source: + - "FileMaker only" + - "No SQL" + - "FM Data API" + - "Exclusive data source" + - "@proofkit/fmdapi" + +metadata: + priority: high + version: 1.0 + \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc b/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc new file mode 100644 index 00000000..78ec63ad --- /dev/null +++ b/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc @@ -0,0 +1,57 @@ +--- +description: +globs: +alwaysApply: false +--- +# UI Components and Styling + +This rule documents the UI component library and styling conventions used in the project. + + +name: ui_components +description: Documents UI component library usage and styling conventions +filters: + - type: file_extension + pattern: "\\.(ts|tsx)$" + - type: directory + pattern: "src/components/" + - type: content + pattern: "@mantine/" + +conventions: + component_library: + - Mantine v7 as primary UI framework + - Tabler icons for iconography + - Mantine React Table for data grids + - Custom components extend Mantine base + + styling: + - PostCSS for processing + - Mantine theme customization + - CSS modules for component styles + - CSS variables for theming + + components: + - Atomic design principles + - Consistent prop interfaces + - Accessibility first + - Responsive design patterns + + forms: + - React Hook Form for form state + - Zod for validation schemas + - Mantine form components + - Custom form layouts + +dependencies: + mantine_core: "^7.17.0" + mantine_hooks: "^7.17.0" + mantine_dates: "^7.17.0" + mantine_notifications: "^7.17.0" + react_hook_form: "^7.54.2" + zod: "^3.24.2" + +metadata: + priority: high + version: 1.0 + \ No newline at end of file diff --git a/packages/cli-old/template/extras/config/drizzle-config-mysql.ts b/packages/cli-old/template/extras/config/drizzle-config-mysql.ts new file mode 100644 index 00000000..1f71d754 --- /dev/null +++ b/packages/cli-old/template/extras/config/drizzle-config-mysql.ts @@ -0,0 +1,12 @@ +import { type Config } from "drizzle-kit"; + +import { env } from "~/env"; + +export default { + schema: "./src/server/db/schema.ts", + dialect: "mysql", + dbCredentials: { + url: env.DATABASE_URL, + }, + tablesFilter: ["project1_*"], +} satisfies Config; diff --git a/packages/cli-old/template/extras/config/drizzle-config-postgres.ts b/packages/cli-old/template/extras/config/drizzle-config-postgres.ts new file mode 100644 index 00000000..d2a21ed7 --- /dev/null +++ b/packages/cli-old/template/extras/config/drizzle-config-postgres.ts @@ -0,0 +1,12 @@ +import { type Config } from "drizzle-kit"; + +import { env } from "~/env"; + +export default { + schema: "./src/server/db/schema.ts", + dialect: "postgresql", + dbCredentials: { + url: env.DATABASE_URL, + }, + tablesFilter: ["project1_*"], +} satisfies Config; diff --git a/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts b/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts new file mode 100644 index 00000000..34f8fa24 --- /dev/null +++ b/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts @@ -0,0 +1,12 @@ +import { type Config } from "drizzle-kit"; + +import { env } from "~/env"; + +export default { + schema: "./src/server/db/schema.ts", + dialect: "sqlite", + dbCredentials: { + url: env.DATABASE_URL, + }, + tablesFilter: ["project1_*"], +} satisfies Config; diff --git a/packages/cli-old/template/extras/config/fmschema.config.mjs b/packages/cli-old/template/extras/config/fmschema.config.mjs new file mode 100644 index 00000000..660edd23 --- /dev/null +++ b/packages/cli-old/template/extras/config/fmschema.config.mjs @@ -0,0 +1,9 @@ +/** @type {import("@proofkit/fmdapi/dist/utils/typegen/types.d.ts").GenerateSchemaOptions} */ +export const config = { + clientSuffix: "Layout", + schemas: [ + // add your layouts and name schemas here + ], + clearOldFiles: true, + path: "./src/config/schemas/filemaker", +}; diff --git a/packages/cli-old/template/extras/config/get-query-client.ts b/packages/cli-old/template/extras/config/get-query-client.ts new file mode 100644 index 00000000..44598cba --- /dev/null +++ b/packages/cli-old/template/extras/config/get-query-client.ts @@ -0,0 +1,6 @@ +import { QueryClient } from "@tanstack/react-query"; +import { cache } from "react"; + +// cache() is scoped per request, so we don't leak data between requests +const getQueryClient = cache(() => new QueryClient()); +export default getQueryClient; diff --git a/packages/cli-old/template/extras/config/postcss.config.cjs b/packages/cli-old/template/extras/config/postcss.config.cjs new file mode 100644 index 00000000..4cdb2f43 --- /dev/null +++ b/packages/cli-old/template/extras/config/postcss.config.cjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +module.exports = config; diff --git a/packages/cli-old/template/extras/config/query-provider-vite.tsx b/packages/cli-old/template/extras/config/query-provider-vite.tsx new file mode 100644 index 00000000..5af4ad27 --- /dev/null +++ b/packages/cli-old/template/extras/config/query-provider-vite.tsx @@ -0,0 +1,17 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +const queryClient = new QueryClient(); + +export default function QueryProvider({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + + ); +} diff --git a/packages/cli-old/template/extras/config/query-provider.tsx b/packages/cli-old/template/extras/config/query-provider.tsx new file mode 100644 index 00000000..2afa87bd --- /dev/null +++ b/packages/cli-old/template/extras/config/query-provider.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +import getQueryClient from "./get-query-client"; + +export default function QueryProvider({ + children, +}: { + children: React.ReactNode; +}) { + const queryClient = getQueryClient(); + + return ( + + {children} + + + ); +} diff --git a/packages/cli-old/template/extras/emailProviders/none/email.tsx b/packages/cli-old/template/extras/emailProviders/none/email.tsx new file mode 100644 index 00000000..21106a19 --- /dev/null +++ b/packages/cli-old/template/extras/emailProviders/none/email.tsx @@ -0,0 +1,24 @@ +import { AuthCodeEmail } from "@/emails/auth-code"; +import { render } from "@react-email/render"; + +export async function sendEmail({ + to, + code, + type, +}: { + to: string; + code: string; + type: "verification" | "password-reset"; +}) { + // this is the HTML body of the email to be send + const body = await render( + + ); + const subject = + type === "verification" ? "Verify Your Email" : "Reset Your Password"; + + // TODO: Customize this function to actually send the email to your users + // Learn more: https://proofkit.dev/auth/fm-addon + console.warn("TODO: Customize this function to actually send to your users"); + console.log(`To ${to}: Your ${type} code is ${code}`); +} diff --git a/packages/cli-old/template/extras/emailProviders/plunk/email.tsx b/packages/cli-old/template/extras/emailProviders/plunk/email.tsx new file mode 100644 index 00000000..ef94053e --- /dev/null +++ b/packages/cli-old/template/extras/emailProviders/plunk/email.tsx @@ -0,0 +1,27 @@ +import { AuthCodeEmail } from "@/emails/auth-code"; +import { render } from "@react-email/render"; + +import { plunk } from "../services/plunk"; + +export async function sendEmail({ + to, + code, + type, +}: { + to: string; + code: string; + type: "verification" | "password-reset"; +}) { + // this is the HTML body of the email to be send + const body = await render( + + ); + const subject = + type === "verification" ? "Verify Your Email" : "Reset Your Password"; + + await plunk.emails.send({ + to, + subject, + body, + }); +} diff --git a/packages/cli-old/template/extras/emailProviders/plunk/service.ts b/packages/cli-old/template/extras/emailProviders/plunk/service.ts new file mode 100644 index 00000000..9f6a3ca6 --- /dev/null +++ b/packages/cli-old/template/extras/emailProviders/plunk/service.ts @@ -0,0 +1,4 @@ +import { env } from "@/config/env"; +import Plunk from "@plunk/node"; + +export const plunk = new Plunk(env.PLUNK_API_KEY); diff --git a/packages/cli-old/template/extras/emailProviders/resend/email.tsx b/packages/cli-old/template/extras/emailProviders/resend/email.tsx new file mode 100644 index 00000000..5ca905b8 --- /dev/null +++ b/packages/cli-old/template/extras/emailProviders/resend/email.tsx @@ -0,0 +1,24 @@ +import { AuthCodeEmail } from "@/emails/auth-code"; + +import { resend } from "../services/resend"; + +export async function sendEmail({ + to, + code, + type, +}: { + to: string; + code: string; + type: "verification" | "password-reset"; +}) { + const subject = + type === "verification" ? "Verify Your Email" : "Reset Your Password"; + + await resend.emails.send({ + // TODO: Change this to our own email after verifying your domain with Resend + from: "ProofKit ", + to, + subject, + react: , + }); +} diff --git a/packages/cli-old/template/extras/emailProviders/resend/service.ts b/packages/cli-old/template/extras/emailProviders/resend/service.ts new file mode 100644 index 00000000..9af08cd1 --- /dev/null +++ b/packages/cli-old/template/extras/emailProviders/resend/service.ts @@ -0,0 +1,4 @@ +import { env } from "@/config/env"; +import { Resend } from "resend"; + +export const resend = new Resend(env.RESEND_API_KEY); diff --git a/packages/cli-old/template/extras/emailTemplates/auth-code.tsx b/packages/cli-old/template/extras/emailTemplates/auth-code.tsx new file mode 100644 index 00000000..e09789c5 --- /dev/null +++ b/packages/cli-old/template/extras/emailTemplates/auth-code.tsx @@ -0,0 +1,137 @@ +import { Body, Container, Head, Heading, Html, Img, Section, Text } from "@react-email/components"; + +interface AuthCodeEmailProps { + validationCode: string; + type: "verification" | "password-reset"; +} + +export const AuthCodeEmail = ({ validationCode, type }: AuthCodeEmailProps) => ( + + + + + 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 new file mode 100644 index 00000000..5c4046aa --- /dev/null +++ b/packages/cli-old/template/extras/emailTemplates/generic.tsx @@ -0,0 +1,113 @@ +import { Body, Button, Container, Head, Heading, Hr, Html, Img, Section, Text } from "@react-email/components"; + +export interface GenericEmailProps { + title?: string; + description?: string; + ctaText?: string; + ctaHref?: string; + footer?: string; +} + +export const GenericEmail = ({ title, description, ctaText, ctaHref, footer }: GenericEmailProps) => ( + + + + + 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.dev", + footer: "You received this email because you signed up for updates.", +} as GenericEmailProps; + +export default GenericEmail; + +const styles = { + main: { + backgroundColor: "#ffffff", + fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", + }, + container: { + backgroundColor: "#ffffff", + border: "1px solid #eee", + borderRadius: "5px", + boxShadow: "0 5px 10px rgba(20,50,70,.2)", + marginTop: "20px", + maxWidth: "520px", + margin: "0 auto", + padding: "48px 32px 36px", + } as React.CSSProperties, + logo: { + margin: "0 auto 12px", + display: "block", + } as React.CSSProperties, + title: { + color: "#111827", + fontSize: "22px", + fontWeight: 600, + lineHeight: "28px", + margin: "8px 0 4px", + textAlign: "center" as const, + }, + description: { + color: "#374151", + fontSize: "15px", + lineHeight: "22px", + margin: "8px 0 0", + textAlign: "center" as const, + }, + ctaSection: { + textAlign: "center" as const, + marginTop: "20px", + }, + ctaButton: { + backgroundColor: "#0a85ea", + color: "#fff", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + textDecoration: "none", + display: "inline-block", + padding: "10px 16px", + borderRadius: "6px", + } as React.CSSProperties, + hr: { + borderColor: "#e5e7eb", + margin: "24px 0 12px", + }, + footer: { + color: "#6b7280", + fontSize: "12px", + lineHeight: "18px", + textAlign: "center" as const, + }, +}; diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts new file mode 100644 index 00000000..49191dfa --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts @@ -0,0 +1,97 @@ +"use server"; + +import { + createEmailVerificationRequest, + sendVerificationEmail, + setEmailVerificationRequestCookie, +} from "@/server/auth/utils/email-verification"; +import { + verifyPasswordHash, + verifyPasswordStrength, +} from "@/server/auth/utils/password"; +import { + createSession, + generateSessionToken, + getCurrentSession, + invalidateUserSessions, + setSessionTokenCookie, +} from "@/server/auth/utils/session"; +import { + checkEmailAvailability, + updateUserPassword, + validateLogin, +} from "@/server/auth/utils/user"; +import { actionClient } from "@/server/safe-action"; +import { redirect } from "next/navigation"; + +import { updateEmailSchema, updatePasswordSchema } from "./schema"; + +export const updateEmailAction = actionClient + .schema(updateEmailSchema) + .action(async ({ parsedInput }) => { + const { session, user } = await getCurrentSession(); + if (session === null) { + return { + message: "Not authenticated", + }; + } + + const { email } = parsedInput; + + const emailAvailable = await checkEmailAvailability(email); + if (!emailAvailable) { + return { + error: "This email is already used", + }; + } + + const verificationRequest = await createEmailVerificationRequest( + user.id, + email + ); + await sendVerificationEmail( + verificationRequest.email, + verificationRequest.code + ); + await setEmailVerificationRequestCookie(verificationRequest); + return redirect("/auth/verify-email"); + }); + +export const updatePasswordAction = actionClient + .schema(updatePasswordSchema) + .action(async ({ parsedInput }) => { + const { confirmNewPassword, currentPassword, newPassword } = parsedInput; + + const { session, user } = await getCurrentSession(); + if (session === null) { + return { + error: "Not authenticated", + }; + } + + const strongPassword = await verifyPasswordStrength(newPassword); + if (!strongPassword) { + return { + error: "Weak password", + }; + } + + const validPassword = Boolean( + await validateLogin(user.email, currentPassword) + ); + if (!validPassword) { + return { + error: "Incorrect password", + }; + } + + await invalidateUserSessions(user.id); + await updateUserPassword(user.id, newPassword); + + const sessionToken = generateSessionToken(); + const newSession = await createSession(sessionToken, user.id); + await setSessionTokenCookie(sessionToken, newSession.expiresAt); + return { + message: "Password updated", + }; + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx new file mode 100644 index 00000000..76431716 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx @@ -0,0 +1,29 @@ +import { getCurrentSession } from "@/server/auth/utils/session"; +import { Anchor, Container, Paper, Stack, Text, Title } from "@mantine/core"; +import { redirect } from "next/navigation"; + +import UpdateEmailForm from "./profile-form"; +import UpdatePasswordForm from "./reset-password-form"; + +// import EmailVerificationForm from "./email-verification-form"; + +export default async function Page() { + const { user } = await getCurrentSession(); + + if (user === null) { + return redirect("/auth/login"); + } + + return ( + + Profile Details + + + + + + + + + ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx new file mode 100644 index 00000000..13e3853a --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Anchor, + Button, + Group, + Paper, + PasswordInput, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; + +import { updateEmailAction } from "./actions"; +import { updateEmailSchema } from "./schema"; + +export default function UpdateEmailForm({ + currentEmail, +}: { + currentEmail: string; +}) { + const { form, handleSubmitWithAction, action } = useHookFormAction( + updateEmailAction, + zodResolver(updateEmailSchema), + { formProps: { defaultValues: { email: currentEmail } } } + ); + + return ( +
+ + + + {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 new file mode 100644 index 00000000..b22bee20 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { showSuccessNotification } from "@/utils/notification-helpers"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Anchor, + Button, + Group, + Paper, + PasswordInput, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; +import { useState } from "react"; + +import { updatePasswordAction } from "./actions"; +import { updatePasswordSchema } from "./schema"; + +export default function UpdatePasswordForm() { + const [showForm, setShowForm] = useState(false); + const { form, handleSubmitWithAction, action } = useHookFormAction( + updatePasswordAction, + zodResolver(updatePasswordSchema), + { + formProps: { defaultValues: {} }, + actionProps: { + onSuccess: ({ data }) => { + if (data?.message) { + showSuccessNotification(data.message); + setShowForm(false); + } + }, + }, + } + ); + + if (!showForm) { + return ( + + + + + ); + } + + return ( +
+ + + + + + + + {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 new file mode 100644 index 00000000..046783e4 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod/v4"; + +export const updateEmailSchema = z.object({ + email: z.string().email(), +}); + +export const updatePasswordSchema = z + .object({ + currentPassword: z.string(), + newPassword: z + .string() + .min(8, { message: "Password must be at least 8 characters long" }) + .max(255, { message: "Password is too long" }), + confirmNewPassword: z.string(), + }) + .refine((data) => data.newPassword === data.confirmNewPassword, { + path: ["confirmNewPassword"], + message: "Passwords do not match", + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts new file mode 100644 index 00000000..78c14d96 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts @@ -0,0 +1,39 @@ +"use server"; + +import { + createPasswordResetSession, + invalidateUserPasswordResetSessions, + sendPasswordResetEmail, + setPasswordResetSessionTokenCookie, +} from "@/server/auth/utils/password-reset"; +import { generateSessionToken } from "@/server/auth/utils/session"; +import { getUserFromEmail } from "@/server/auth/utils/user"; +import { actionClient } from "@/server/safe-action"; +import { redirect } from "next/navigation"; + +import { forgotPasswordSchema } from "./schema"; + +export const forgotPasswordAction = actionClient + .schema(forgotPasswordSchema) + .action(async ({ parsedInput }) => { + const { email } = parsedInput; + + const user = await getUserFromEmail(email); + if (user === null) { + return { + error: "Account does not exist", + }; + } + + await invalidateUserPasswordResetSessions(user.id); + const sessionToken = generateSessionToken(); + const session = await createPasswordResetSession( + sessionToken, + user.id, + user.email + ); + + await sendPasswordResetEmail(session.email, session.code); + await setPasswordResetSessionTokenCookie(sessionToken, session.expires_at); + return redirect("/auth/reset-password/verify-email"); + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx new file mode 100644 index 00000000..5ff49868 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button, Paper, Stack, Text, TextInput } from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; + +import { forgotPasswordAction } from "./actions"; +import { forgotPasswordSchema } from "./schema"; + +export default function ForgotForm() { + const { form, handleSubmitWithAction, action } = useHookFormAction( + forgotPasswordAction, + zodResolver(forgotPasswordSchema), + {} + ); + + return ( +
+ + + + + {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 new file mode 100644 index 00000000..09be86ba --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx @@ -0,0 +1,22 @@ +import { Anchor, Container, Text, Title } from "@mantine/core"; + +import ForgotForm from "./forgot-form"; + +export default async function Page() { + return ( + + Forgot Password + + Enter your email for a link to reset your password. + + + + + + + Back to login + + + + ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts new file mode 100644 index 00000000..15829b1a --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts @@ -0,0 +1,5 @@ +import { z } from "zod/v4"; + +export const forgotPasswordSchema = z.object({ + email: z.string().email(), +}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts new file mode 100644 index 00000000..ca66a9df --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts @@ -0,0 +1,35 @@ +"use server"; + +import { getRedirectCookie } from "@/server/auth/utils/redirect"; +import { + createSession, + generateSessionToken, + setSessionTokenCookie, +} from "@/server/auth/utils/session"; +import { validateLogin } from "@/server/auth/utils/user"; +import { actionClient } from "@/server/safe-action"; +import { redirect } from "next/navigation"; + +import { loginSchema } from "./schema"; + +export const loginAction = actionClient + .schema(loginSchema) + .action(async ({ parsedInput }) => { + const { email, password } = parsedInput; + const user = await validateLogin(email, password); + + if (user === null) { + return { error: "Invalid email or password" }; + } + + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, user.id); + setSessionTokenCookie(sessionToken, session.expiresAt); + + if (!user.emailVerified) { + return redirect("/auth/verify-email"); + } + + const redirectTo = await getRedirectCookie(); + return redirect(redirectTo); + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx new file mode 100644 index 00000000..fa13fbb5 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Anchor, + Button, + Group, + Paper, + PasswordInput, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; + +import { loginAction } from "./actions"; +import { loginSchema } from "./schema"; + +export default function LoginForm() { + const { form, handleSubmitWithAction, action } = useHookFormAction( + loginAction, + zodResolver(loginSchema), + {} + ); + + return ( +
+ + + + + + + + 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 new file mode 100644 index 00000000..a98eb6c3 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx @@ -0,0 +1,27 @@ +import { getCurrentSession } from "@/server/auth/utils/session"; +import { Anchor, Container, Text, Title } from "@mantine/core"; +import { redirect } from "next/navigation"; + +import LoginForm from "./login-form"; + +export default async function Page() { + const { session } = await getCurrentSession(); + + if (session !== null) { + return redirect("/"); + } + + return ( + + Welcome back! + + Do not have an account yet?{" "} + + Create account + + + + + + ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts new file mode 100644 index 00000000..66276d2f --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts @@ -0,0 +1,6 @@ +import { z } from "zod/v4"; + +export const loginSchema = z.object({ + email: z.string().email(), + password: z.string(), +}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts new file mode 100644 index 00000000..a781546c --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts @@ -0,0 +1,53 @@ +"use server"; + +import { verifyPasswordStrength } from "@/server/auth/utils/password"; +import { + deletePasswordResetSessionTokenCookie, + invalidateUserPasswordResetSessions, + validatePasswordResetSessionRequest, +} from "@/server/auth/utils/password-reset"; +import { + createSession, + generateSessionToken, + invalidateUserSessions, + setSessionTokenCookie, +} from "@/server/auth/utils/session"; +import { updateUserPassword } from "@/server/auth/utils/user"; +import { actionClient } from "@/server/safe-action"; +import { redirect } from "next/navigation"; + +import { resetPasswordSchema } from "./schema"; + +export const resetPasswordAction = actionClient + .schema(resetPasswordSchema) + .action(async ({ parsedInput }) => { + const { password } = parsedInput; + const { session: passwordResetSession, user } = + await validatePasswordResetSessionRequest(); + if (passwordResetSession === null) { + return { + error: "Not authenticated", + }; + } + if (!passwordResetSession.email_verified) { + return { + error: "Forbidden", + }; + } + + const strongPassword = await verifyPasswordStrength(password); + if (!strongPassword) { + return { + error: "Weak password", + }; + } + await invalidateUserPasswordResetSessions(passwordResetSession.id_user); + await invalidateUserSessions(passwordResetSession.id_user); + await updateUserPassword(passwordResetSession.id_user, password); + + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, user.id); + await setSessionTokenCookie(sessionToken, session.expiresAt); + await deletePasswordResetSessionTokenCookie(); + return redirect("/"); + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx new file mode 100644 index 00000000..9a164a1d --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx @@ -0,0 +1,33 @@ +import { env } from "@/config/env"; +import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset"; +import { Alert, Anchor, Container, Text, Title } from "@mantine/core"; +import { redirect } from "next/navigation"; + +import ResetPasswordForm from "./reset-password-form"; + +export default async function Page() { + const { session, user } = await validatePasswordResetSessionRequest(); + if (session === null) { + return redirect("/auth/forgot-password"); + } + if (!session.email_verified) { + return redirect("/auth/reset-password/verify-email"); + } + + return ( + + Reset Password + + Enter your new password. + + + + + + + Back to login + + + + ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx new file mode 100644 index 00000000..e11b3acd --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Button, + Paper, + PasswordInput, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; + +import { resetPasswordAction } from "./actions"; +import { resetPasswordSchema } from "./schema"; + +export default function ForgotForm() { + const { form, handleSubmitWithAction, action } = useHookFormAction( + resetPasswordAction, + zodResolver(resetPasswordSchema), + {} + ); + + return ( +
+ + + + + + + {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 new file mode 100644 index 00000000..8315fd2c --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod/v4"; + +export const resetPasswordSchema = z + .object({ + password: z + .string() + .min(8, { message: "Your password should be at least 8 characters" }) + .max(255, { message: "Password is too long" }), + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + path: ["confirmPassword"], + message: "Passwords do not match", + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts new file mode 100644 index 00000000..4ce0b1b7 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts @@ -0,0 +1,46 @@ +"use server"; + +import { + setPasswordResetSessionAsEmailVerified, + validatePasswordResetSessionRequest, +} from "@/server/auth/utils/password-reset"; +import { setUserAsEmailVerifiedIfEmailMatches } from "@/server/auth/utils/user"; +import { actionClient } from "@/server/safe-action"; +import { redirect } from "next/navigation"; + +import { verifyEmailSchema } from "./schema"; + +export const verifyEmailAction = actionClient + .schema(verifyEmailSchema) + .action(async ({ parsedInput }) => { + const { session } = await validatePasswordResetSessionRequest(); + if (session === null) { + return { + error: "Not authenticated", + }; + } + if (Boolean(session.email_verified)) { + return { + error: "Forbidden", + }; + } + + const { code } = parsedInput; + + if (code !== session.code) { + return { + error: "Incorrect code", + }; + } + await setPasswordResetSessionAsEmailVerified(session.id); + const emailMatches = await setUserAsEmailVerifiedIfEmailMatches( + session.id_user, + session.email + ); + if (!emailMatches) { + return { + error: "Please restart the process", + }; + } + return redirect("/auth/reset-password"); + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx new file mode 100644 index 00000000..b3796b06 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx @@ -0,0 +1,33 @@ +import { env } from "@/config/env"; +import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset"; +import { Alert, Anchor, Container, Text, Title } from "@mantine/core"; +import { redirect } from "next/navigation"; + +import VerifyEmailForm from "./verify-email-form"; + +export default async function Page() { + const { session } = await validatePasswordResetSessionRequest(); + if (session === null) { + return redirect("/auth/forgot-password"); + } + if (session.email_verified) { + return redirect("/auth/reset-password"); + } + + return ( + + Verify Email + + Enter the code sent to your email. + + + + + + + Back to login + + + + ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts new file mode 100644 index 00000000..37d5311a --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts @@ -0,0 +1,5 @@ +import { z } from "zod/v4"; + +export const verifyEmailSchema = z.object({ + code: z.string().length(8), +}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx new file mode 100644 index 00000000..2d454b7e --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button, Paper, PinInput, Stack, Text } from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; + +import { verifyEmailAction } from "./actions"; +import { verifyEmailSchema } from "./schema"; + +export default function VerifyEmailForm() { + const { form, handleSubmitWithAction, action } = useHookFormAction( + verifyEmailAction, + zodResolver(verifyEmailSchema), + {} + ); + + return ( +
+ + + { + 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 new file mode 100644 index 00000000..3faa5d0f --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts @@ -0,0 +1,50 @@ +"use server"; + +import { + createEmailVerificationRequest, + sendVerificationEmail, + setEmailVerificationRequestCookie, +} from "@/server/auth/utils/email-verification"; +import { verifyPasswordStrength } from "@/server/auth/utils/password"; +import { + createSession, + generateSessionToken, + setSessionTokenCookie, +} from "@/server/auth/utils/session"; +import { checkEmailAvailability, createUser } from "@/server/auth/utils/user"; +import { actionClient } from "@/server/safe-action"; +import { redirect } from "next/navigation"; + +import { signupSchema } from "./schema"; + +export const signupAction = actionClient + .schema(signupSchema) + .action(async ({ parsedInput }) => { + const { email, password } = parsedInput; + const emailAvailable = await checkEmailAvailability(email); + if (!emailAvailable) { + return { error: "Email already in use" }; + } + + const passwordStrong = await verifyPasswordStrength(password); + if (!passwordStrong) { + return { error: "Password is too weak" }; + } + + const user = await createUser(email, password); + const emailVerificationRequest = await createEmailVerificationRequest( + user.id, + user.email + ); + await sendVerificationEmail( + emailVerificationRequest.email, + emailVerificationRequest.code + ); + await setEmailVerificationRequestCookie(emailVerificationRequest); + + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, user.id); + setSessionTokenCookie(sessionToken, session.expiresAt); + + return redirect("/auth/verify-email"); + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx new file mode 100644 index 00000000..056d5284 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx @@ -0,0 +1,27 @@ +import { getCurrentSession } from "@/server/auth/utils/session"; +import { Anchor, Container, Text, Title } from "@mantine/core"; +import { redirect } from "next/navigation"; + +import SignupForm from "./signup-form"; + +export default async function Page() { + const { session } = await getCurrentSession(); + + if (session !== null) { + return redirect("/"); + } + + return ( + + Create account + + Already have an account?{" "} + + Sign in + + + + + + ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts new file mode 100644 index 00000000..e15638ca --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod/v4"; + +export const signupSchema = z + .object({ + email: z.string().email(), + password: z.string().min(8), + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + path: ["confirmPassword"], + message: "Passwords do not match", + }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx new file mode 100644 index 00000000..f41454ae --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Anchor, + Button, + Group, + Paper, + PasswordInput, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; + +import { signupAction } from "./actions"; +import { signupSchema } from "./schema"; + +export default function SignupForm() { + const { form, handleSubmitWithAction, action } = useHookFormAction( + signupAction, + zodResolver(signupSchema), + {} + ); + + return ( +
+ + + + + + {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 new file mode 100644 index 00000000..3ad9697a --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts @@ -0,0 +1,109 @@ +"use server"; + +import { + createEmailVerificationRequest, + deleteEmailVerificationRequestCookie, + deleteUserEmailVerificationRequest, + getUserEmailVerificationRequestFromRequest, + sendVerificationEmail, + setEmailVerificationRequestCookie, +} from "@/server/auth/utils/email-verification"; +import { invalidateUserPasswordResetSessions } from "@/server/auth/utils/password-reset"; +import { getRedirectCookie } from "@/server/auth/utils/redirect"; +import { getCurrentSession } from "@/server/auth/utils/session"; +import { updateUserEmailAndSetEmailAsVerified } from "@/server/auth/utils/user"; +import { actionClient } from "@/server/safe-action"; +import { redirect } from "next/navigation"; + +import { emailVerificationSchema } from "./schema"; + +export const verifyEmailAction = actionClient + .schema(emailVerificationSchema) + .action(async ({ parsedInput, ctx }) => { + const { session, user } = await getCurrentSession(); + if (session === null) { + return { + error: "Not authenticated", + }; + } + + let verificationRequest = + await getUserEmailVerificationRequestFromRequest(); + if (verificationRequest === null) { + return { + error: "Not authenticated", + }; + } + const { code } = parsedInput; + if (verificationRequest.expires_at === null) { + return { + error: "Verification code expired", + }; + } + + if (Date.now() >= verificationRequest.expires_at * 1000) { + verificationRequest = await createEmailVerificationRequest( + verificationRequest.id_user, + verificationRequest.email + ); + await sendVerificationEmail( + verificationRequest.email, + verificationRequest.code + ); + return { + error: + "The verification code was expired. We sent another code to your inbox.", + }; + } + if (verificationRequest.code !== code) { + return { + error: "Incorrect code.", + }; + } + await deleteUserEmailVerificationRequest(user.id); + await invalidateUserPasswordResetSessions(user.id); + await updateUserEmailAndSetEmailAsVerified( + user.id, + verificationRequest.email + ); + await deleteEmailVerificationRequestCookie(); + + const redirectTo = await getRedirectCookie(); + return redirect(redirectTo); + }); + +export const resendEmailVerificationAction = actionClient.action(async () => { + const { session, user } = await getCurrentSession(); + if (session === null) { + return { + error: "Not authenticated", + }; + } + + let verificationRequest = await getUserEmailVerificationRequestFromRequest(); + if (verificationRequest === null) { + if (user.emailVerified) { + return { + error: "Forbidden", + }; + } + + verificationRequest = await createEmailVerificationRequest( + user.id, + user.email + ); + } else { + verificationRequest = await createEmailVerificationRequest( + user.id, + verificationRequest.email + ); + } + await sendVerificationEmail( + verificationRequest.email, + verificationRequest.code + ); + await setEmailVerificationRequestCookie(verificationRequest); + return { + message: "A new code was sent to your inbox.", + }; +}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx new file mode 100644 index 00000000..3108c3fa --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button, Paper, PinInput, Stack, Text } from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; + +import { verifyEmailAction } from "./actions"; +import { emailVerificationSchema } from "./schema"; + +export default function LoginForm() { + const { form, handleSubmitWithAction, action } = useHookFormAction( + verifyEmailAction, + zodResolver(emailVerificationSchema), + {} + ); + + return ( +
+ + + { + 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 new file mode 100644 index 00000000..bfad170d --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx @@ -0,0 +1,40 @@ +import { getUserEmailVerificationRequestFromRequest } from "@/server/auth/utils/email-verification"; +import { getRedirectCookie } from "@/server/auth/utils/redirect"; +import { getCurrentSession } from "@/server/auth/utils/session"; +import { Anchor, Container, Text, Title } from "@mantine/core"; +import { redirect } from "next/navigation"; + +import EmailVerificationForm from "./email-verification-form"; +import ResendButton from "./resend-button"; + +export default async function Page() { + const { user } = await getCurrentSession(); + + if (user === null) { + return redirect("/auth/login"); + } + + // TODO: Ideally we'd sent a new verification email automatically if the previous one is expired, + // but we can't set cookies inside server components. + const verificationRequest = + await getUserEmailVerificationRequestFromRequest(); + if (verificationRequest === null && user.emailVerified) { + const redirectTo = await getRedirectCookie(); + return redirect(redirectTo); + } + + return ( + + Verify your email + + Enter the code sent to {verificationRequest?.email ?? user.email} + + + Change email + + + + + + ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx new file mode 100644 index 00000000..ee36ae70 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { Alert, Anchor, Button, Group, Stack, Text } from "@mantine/core"; +import { useAction } from "next-safe-action/hooks"; + +import { resendEmailVerificationAction } from "./actions"; + +export default function ResendButton() { + const action = useAction(resendEmailVerificationAction); + return ( + + + + {"Didn't receive the email?"} + + + + + {action.result.data?.message && ( + {action.result.data.message} + )} + + {action.result.data?.error && ( + + {action.result.data.error} + + )} + + ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts new file mode 100644 index 00000000..d962f424 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts @@ -0,0 +1,5 @@ +import { z } from "zod/v4"; + +export const emailVerificationSchema = z.object({ + code: z.string().length(8), +}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts new file mode 100644 index 00000000..c4e4c11f --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts @@ -0,0 +1,19 @@ +"use server"; + +import { + getCurrentSession, + invalidateSession, +} from "@/server/auth/utils/session"; +import { redirect } from "next/navigation"; + +export async function currentSessionAction() { + return await getCurrentSession(); +} + +export async function logoutAction() { + const { session } = await currentSessionAction(); + if (session) { + await invalidateSession(session.id); + } + redirect("/"); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx new file mode 100644 index 00000000..9bce1e21 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx @@ -0,0 +1,18 @@ +import { getCurrentSession } from "@/server/auth/utils/session"; + +import AuthRedirect from "./redirect"; + +/** + * This server component will protect the contents of it's children from users who aren't logged in + * It will redirect to the login page if the user is not logged in, or the verify email page if the user is logged in but hasn't verified their email + */ +export default async function Protect({ + children, +}: { + children: React.ReactNode; +}) { + const { session, user } = await getCurrentSession(); + if (!session) return ; + if (!user.emailVerified) return ; + return <>{children}; +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx new file mode 100644 index 00000000..40a2afef --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { Center, Loader } from "@mantine/core"; +import Cookies from "js-cookie"; +import { redirect } from "next/navigation"; +import { useEffect } from "react"; + +/** + * A client-side component that redirects to the given path, but saves the current path in the redirectTo cookie. + */ +export default function AuthRedirect({ path }: { path: string }) { + useEffect(() => { + if (typeof window !== "undefined") { + Cookies.set("redirectTo", window.location.pathname, { + expires: 1 / 24 / 60, // 1 hour + }); + redirect(path); + } + }, []); + + return ( +
+ +
+ ); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts b/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts new file mode 100644 index 00000000..46bec5b2 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts @@ -0,0 +1,60 @@ +import { Session } from "@/server/auth/utils/session"; +import { User } from "@/server/auth/utils/user"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { currentSessionAction, logoutAction } from "./actions"; + +type LogoutAction = () => Promise; +type UseUserResult = + | { + state: "authenticated"; + session: Session; + user: User; + logout: LogoutAction; + } + | { + state: "unauthenticated"; + session: null; + user: null; + logout: LogoutAction; + } + | { state: "loading"; session: null; user: null; logout: LogoutAction }; + +export function useUser(): UseUserResult { + const query = useQuery({ + queryKey: ["current-user"], + queryFn: () => currentSessionAction(), + retry: false, + }); + const queryClient = useQueryClient(); + + const { mutateAsync } = useMutation({ + mutationFn: logoutAction, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: ["current-user"] }); + queryClient.setQueryData(["current-user"], { session: null, user: null }); + }, + onSettled: () => + queryClient.invalidateQueries({ queryKey: ["current-user"] }), + }); + + const defaultResult: UseUserResult = { + state: "unauthenticated", + session: null, + user: null, + logout: mutateAsync, + }; + + if (query.isLoading) { + return { ...defaultResult, state: "loading" }; + } + if (query.data?.session) { + return { + ...defaultResult, + state: "authenticated", + session: query.data.session, + user: query.data.user, + }; + } + return defaultResult; +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx new file mode 100644 index 00000000..e4fd0778 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Button, Menu, px, Skeleton } from "@mantine/core"; +import { IconChevronDown, IconLogout, IconUser } from "@tabler/icons-react"; +import Link from "next/link"; + +import { useUser } from "./use-user"; + +export default function UserMenu() { + const { state, session, user, logout } = useUser(); + + if (state === "loading") { + return ; + } + if (state === "unauthenticated") { + return ( + + ); + } + return ( + + + + + + } + > + 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 new file mode 100644 index 00000000..e09789c5 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx @@ -0,0 +1,137 @@ +import { Body, Container, Head, Heading, Html, Img, Section, Text } from "@react-email/components"; + +interface AuthCodeEmailProps { + validationCode: string; + type: "verification" | "password-reset"; +} + +export const AuthCodeEmail = ({ validationCode, type }: AuthCodeEmailProps) => ( + + + + + 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 new file mode 100644 index 00000000..86ea06f7 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/middleware.ts @@ -0,0 +1,44 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export async function middleware(request: NextRequest): Promise { + if (request.method === "GET") { + const response = NextResponse.next(); + const token = request.cookies.get("session")?.value ?? null; + if (token !== null) { + // Only extend cookie expiration on GET requests since we can be sure + // a new session wasn't set when handling the request. + response.cookies.set("session", token, { + path: "/", + maxAge: 60 * 60 * 24 * 30, + sameSite: "lax", + httpOnly: true, + secure: process.env.NODE_ENV === "production", + }); + } + return response; + } + + const originHeader = request.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = request.headers.get("Host"); + if (originHeader === null || hostHeader === null) { + return new NextResponse(null, { + status: 403, + }); + } + let origin: URL; + try { + origin = new URL(originHeader); + } catch { + return new NextResponse(null, { + status: 403, + }); + } + if (origin.host !== hostHeader) { + return new NextResponse(null, { + status: 403, + }); + } + return NextResponse.next(); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts new file mode 100644 index 00000000..253eab74 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts @@ -0,0 +1,137 @@ +import { encodeBase32 } from "@oslojs/encoding"; +import { cookies } from "next/headers"; + +import { emailVerificationLayout } from "../db/client"; +import { TemailVerification } from "../db/emailVerification"; +import { sendEmail } from "../email"; +import { generateRandomOTP } from "./index"; +import { getCurrentSession } from "./session"; + +/** + * An Email Verification Request is a record in the email verification table that is created when a user requests to change their email address. It's like a temporary session which can expire if the user doesn't verify the new email address within a certain amount of time. + */ + +/** + * Get a user's email verification request. + * @param userId - The ID of the user. + * @param id - The ID of the email verification request. + * @returns The email verification request, or null if it doesn't exist. + */ +export async function getUserEmailVerificationRequest( + userId: string, + id: string +): Promise { + const result = await emailVerificationLayout.maybeFindFirst({ + query: { id_user: `==${userId}`, id: `==${id}` }, + }); + return result?.data.fieldData ?? null; +} + +/** + * Create a new email verification request for a user. + * @param id_user - The ID of the user. + * @param email - The email address to verify. + * @returns The email verification request. + */ +export async function createEmailVerificationRequest( + id_user: string, + email: string +): Promise { + deleteUserEmailVerificationRequest(id_user); + const idBytes = new Uint8Array(20); + crypto.getRandomValues(idBytes); + const id = encodeBase32(idBytes).toLowerCase(); + + const code = generateRandomOTP(); + const expiresAt = new Date(Date.now() + 1000 * 60 * 10); + + const request: TemailVerification = { + id, + code, + expires_at: Math.floor(expiresAt.getTime() / 1000), + email, + id_user, + }; + + await emailVerificationLayout.create({ + fieldData: request, + }); + + return request; +} + +/** + * Delete a user's email verification request. + * @param id_user - The ID of the user. + */ +export async function deleteUserEmailVerificationRequest( + id_user: string +): Promise { + const result = await emailVerificationLayout.maybeFindFirst({ + query: { id_user: `==${id_user}` }, + }); + if (result === null) return; + + await emailVerificationLayout.delete({ recordId: result.data.recordId }); +} + +/** + * Send a verification email to a user. + * @param email - The email address to send the verification email to. + * @param code - The verification code to send to the user. + */ +export async function sendVerificationEmail( + email: string, + code: string +): Promise { + await sendEmail({ to: email, code, type: "verification" }); +} + +/** + * Set a cookie for a user's email verification request. + * @param request - The email verification request. + */ +export async function setEmailVerificationRequestCookie( + request: TemailVerification +): Promise { + (await cookies()).set("email_verification", request.id, { + httpOnly: true, + path: "/", + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + expires: request.expires_at + ? new Date(request.expires_at * 1000) + : new Date(Date.now() + 1000 * 60 * 60), + }); +} + +/** + * Delete the cookie for a user's email verification request. + */ +export async function deleteEmailVerificationRequestCookie(): Promise { + (await cookies()).set("email_verification", "", { + httpOnly: true, + path: "/", + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 0, + }); +} + +/** + * Get a user's email verification request from the cookie. + * @returns The email verification request, or null if it doesn't exist. + */ +export async function getUserEmailVerificationRequestFromRequest(): Promise { + const { user } = await getCurrentSession(); + if (user === null) { + return null; + } + const id = (await cookies()).get("email_verification")?.value ?? null; + if (id === null) { + return null; + } + const request = await getUserEmailVerificationRequest(user.id, id); + + return request; +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts new file mode 100644 index 00000000..377f773d --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts @@ -0,0 +1,51 @@ +import { createCipheriv, createDecipheriv } from "crypto"; +import { DynamicBuffer } from "@oslojs/binary"; +import { decodeBase64 } from "@oslojs/encoding"; + +const key = decodeBase64(process.env.ENCRYPTION_KEY ?? ""); + +export function encrypt(data: Uint8Array): Uint8Array { + const iv = new Uint8Array(16); + crypto.getRandomValues(iv); + const cipher = createCipheriv("aes-128-gcm", key, iv); + const encrypted = new DynamicBuffer(0); + encrypted.write(iv); + encrypted.write(cipher.update(data)); + encrypted.write(cipher.final()); + encrypted.write(cipher.getAuthTag()); + return encrypted.bytes(); +} + +/** + * Encrypt a string for storage in the database. + * Here we're returning a base64 encoded string since FileMaker doesn't store binary data. + * @param data - The string to encrypt. + * @returns The encrypted string. + */ +export function encryptString(data: string): string { + const encrypted = encrypt(new TextEncoder().encode(data)); + return Buffer.from(encrypted).toString("base64"); +} + +/** + * Decrypt a string stored in the database. + * @param encrypted - The encrypted string to decrypt. + * @returns The decrypted string. + */ +export function decrypt(encrypted: Uint8Array): Uint8Array { + if (encrypted.byteLength < 33) { + throw new Error("Invalid data"); + } + const decipher = createDecipheriv("aes-128-gcm", key, encrypted.slice(0, 16)); + decipher.setAuthTag(encrypted.slice(encrypted.byteLength - 16)); + const decrypted = new DynamicBuffer(0); + decrypted.write( + decipher.update(encrypted.slice(16, encrypted.byteLength - 16)) + ); + decrypted.write(decipher.final()); + return decrypted.bytes(); +} + +export function decryptToString(data: Uint8Array): string { + return new TextDecoder().decode(decrypt(data)); +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts new file mode 100644 index 00000000..41849aef --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts @@ -0,0 +1,16 @@ +import { encodeBase32UpperCaseNoPadding } from "@oslojs/encoding"; + +export function generateRandomOTP(): string { + const bytes = new Uint8Array(5); + crypto.getRandomValues(bytes); + const code = encodeBase32UpperCaseNoPadding(bytes); + return code; +} + +export const options = { + password: { + minLength: 8, + maxLength: 255, + checkCompromised: false, // set to true to prevent known compromised passwords on signup + }, +}; diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts new file mode 100644 index 00000000..d3434460 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts @@ -0,0 +1,153 @@ +import { sha256 } from "@oslojs/crypto/sha2"; +import { encodeHexLowerCase } from "@oslojs/encoding"; +import { cookies } from "next/headers"; + +import { passwordResetLayout } from "../db/client"; +import { TpasswordReset } from "../db/passwordReset"; +import { sendEmail } from "../email"; +import { generateRandomOTP } from "./index"; +import type { User } from "./user"; + +type PasswordResetSession = Omit< + TpasswordReset, + | "proofkit_auth_users::email" + | "proofkit_auth_users::emailVerified" + | "proofkit_auth_users::username" +>; + +export async function createPasswordResetSession( + token: string, + id_user: string, + email: string +): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session: PasswordResetSession = { + id: sessionId, + id_user, + email, + expires_at: Math.floor( + new Date(Date.now() + 1000 * 60 * 10).getTime() / 1000 + ), + code: generateRandomOTP(), + email_verified: 0, + }; + await passwordResetLayout.create({ fieldData: session }); + + return session; +} + +/** + * Validate a password reset session token. + * @param token - The password reset session token. + * @returns The password reset session, or null if it doesn't exist. + */ +export async function validatePasswordResetSessionToken( + token: string +): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const row = await passwordResetLayout.maybeFindFirst({ + query: { id: `==${sessionId}` }, + }); + + if (row === null) { + return { session: null, user: null }; + } + const session: PasswordResetSession = { + id: row.data.fieldData.id, + id_user: row.data.fieldData.id_user, + email: row.data.fieldData.email, + code: row.data.fieldData.code, + expires_at: row.data.fieldData.expires_at, + email_verified: row.data.fieldData.email_verified, + }; + + const user: User = { + id: row.data.fieldData.id_user, + email: row.data.fieldData["proofkit_auth_users::email"], + username: row.data.fieldData["proofkit_auth_users::username"], + emailVerified: Boolean( + row.data.fieldData["proofkit_auth_users::emailVerified"] + ), + }; + if (session.expires_at && Date.now() >= session.expires_at * 1000) { + await passwordResetLayout.delete({ recordId: row.data.recordId }); + return { session: null, user: null }; + } + return { session, user }; +} + +async function fetchPasswordResetSession(sessionId: string) { + return ( + await passwordResetLayout.findOne({ query: { id: `==${sessionId}` } }) + ).data; +} + +export async function setPasswordResetSessionAsEmailVerified( + sessionId: string +): Promise { + const { recordId } = await fetchPasswordResetSession(sessionId); + await passwordResetLayout.update({ + recordId, + fieldData: { email_verified: 1 }, + }); +} + +export async function invalidateUserPasswordResetSessions( + userId: string +): Promise { + const sessions = await passwordResetLayout.find({ + query: { id_user: `==${userId}` }, + ignoreEmptyResult: true, + }); + for (const session of sessions.data) { + await passwordResetLayout.delete({ recordId: session.recordId }); + } +} + +export async function validatePasswordResetSessionRequest(): Promise { + const token = (await cookies()).get("password_reset_session")?.value ?? null; + if (token === null) { + return { session: null, user: null }; + } + const result = await validatePasswordResetSessionToken(token); + if (result.session === null) { + deletePasswordResetSessionTokenCookie(); + } + return result; +} + +export async function setPasswordResetSessionTokenCookie( + token: string, + expiresAt: number | null +): Promise { + (await cookies()).set("password_reset_session", token, { + expires: expiresAt + ? new Date(expiresAt * 1000) + : new Date(Date.now() + 60 * 60 * 1000), + sameSite: "lax", + httpOnly: true, + path: "/", + secure: process.env.NODE_ENV === "production", + }); +} + +export async function deletePasswordResetSessionTokenCookie(): Promise { + (await cookies()).set("password_reset_session", "", { + maxAge: 0, + sameSite: "lax", + httpOnly: true, + path: "/", + secure: process.env.NODE_ENV === "production", + }); +} + +export async function sendPasswordResetEmail( + email: string, + code: string +): Promise { + await sendEmail({ to: email, code, type: "password-reset" }); +} + +export type PasswordResetSessionValidationResult = + | { session: PasswordResetSession; user: User } + | { session: null; user: null }; diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts new file mode 100644 index 00000000..bf723a6f --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts @@ -0,0 +1,67 @@ +import { options } from "."; +import { hash, verify } from "@node-rs/argon2"; +import { sha1 } from "@oslojs/crypto/sha1"; +import { encodeHexLowerCase } from "@oslojs/encoding"; + +/** + * Hash a password using Argon2. + * @param password - The password to hash. + * @returns The hashed password. + */ +export async function hashPassword(password: string): Promise { + return await hash(password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); +} + +/** + * Verify that a password matches a hash. + * @param hash - The hash to verify against. + * @param password - The password to verify. + * @returns True if the password matches the hash, false otherwise. + */ +export async function verifyPasswordHash( + hash: string, + password: string +): Promise { + return await verify(hash, password); +} + +/** + * Verify that a password is strong enough. + * @param password - The password to verify. + * @returns True if the password is strong enough, false otherwise. + */ +export async function verifyPasswordStrength( + password: string +): Promise { + if ( + password.length < options.password.minLength || + password.length > options.password.maxLength + ) { + return false; + } + + if (options.password.checkCompromised) { + const hash = encodeHexLowerCase(sha1(new TextEncoder().encode(password))); + const hashPrefix = hash.slice(0, 5); + const response = await fetch( + `https://api.pwnedpasswords.com/range/${hashPrefix}` + ); + const data = await response.text(); + const items = data.split("\n"); + for (const item of items) { + const hashSuffix = item.slice(0, 35).toLowerCase(); + if (hash === hashPrefix + hashSuffix) { + console.log( + "User's new password was found in list of compromised passwords, reject" + ); + return false; + } + } + } + return true; +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts new file mode 100644 index 00000000..eb3b467b --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts @@ -0,0 +1,8 @@ +import { cookies } from "next/headers"; + +export async function getRedirectCookie() { + const cookieStore = await cookies(); + const redirectTo = cookieStore.get("redirectTo")?.value; + cookieStore.delete("redirectTo"); + return redirectTo ?? "/"; +} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts new file mode 100644 index 00000000..aaa80f35 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts @@ -0,0 +1,191 @@ +import { sha256 } from "@oslojs/crypto/sha2"; +import { + encodeBase32LowerCaseNoPadding, + encodeHexLowerCase, +} from "@oslojs/encoding"; +import { cookies } from "next/headers"; +import { cache } from "react"; + +import { sessionsLayout } from "../db/client"; +import { Tsessions as _Session } from "../db/sessions"; +import type { User } from "./user"; + +/** + * Generate a random session token with sufficient entropy for a session ID. + * @returns The session token. + */ +export function generateSessionToken(): string { + const bytes = new Uint8Array(20); + crypto.getRandomValues(bytes); + const token = encodeBase32LowerCaseNoPadding(bytes); + return token; +} + +/** + * Create a new session for a user and save it to the database. + * @param token - The session token. + * @param userId - The ID of the user. + * @returns The session. + */ +export async function createSession( + token: string, + userId: string +): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session: Session = { + id: sessionId, + id_user: userId, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), + }; + + // create session in DB + await sessionsLayout.create({ + fieldData: { + id: session.id, + id_user: session.id_user, + expiresAt: Math.floor(session.expiresAt.getTime() / 1000), + }, + }); + + return session; +} + +/** + * Invalidate a session by deleting it from the database. + * @param sessionId - The ID of the session to invalidate. + */ +export async function invalidateSession(sessionId: string): Promise { + const fmResult = await sessionsLayout.maybeFindFirst({ + query: { id: `==${sessionId}` }, + }); + if (fmResult === null) { + return; + } + await sessionsLayout.delete({ recordId: fmResult.data.recordId }); +} + +/** + * Validate a session token to make sure it still exists in the database and hasn't expired. + * @param token - The session token. + * @returns The session, or null if it doesn't exist. + */ +export async function validateSessionToken( + token: string +): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + + const result = await sessionsLayout.maybeFindFirst({ + query: { id: `==${sessionId}` }, + }); + if (result === null) { + return { session: null, user: null }; + } + + const fmResult = result.data.fieldData; + const recordId = result.data.recordId; + const session: Session = { + id: fmResult.id, + id_user: fmResult.id_user, + expiresAt: fmResult.expiresAt + ? new Date(fmResult.expiresAt * 1000) + : new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), + }; + + const user: User = { + id: session.id_user, + email: fmResult["proofkit_auth_users::email"], + emailVerified: Boolean(fmResult["proofkit_auth_users::emailVerified"]), + username: fmResult["proofkit_auth_users::username"], + }; + + // delete session if it has expired + if (Date.now() >= session.expiresAt.getTime()) { + await sessionsLayout.delete({ recordId }); + return { session: null, user: null }; + } + + // extend session if it's going to expire soon + // You may want to customize this logic to better suit your app's requirements + if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { + session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); + await sessionsLayout.update({ + recordId, + fieldData: { + expiresAt: Math.floor(session.expiresAt.getTime() / 1000), + }, + }); + } + + return { session, user }; +} + +/** + * Get the current session from the cookie. + * Wrapped in a React cache to avoid calling the database more than once per request + * This function can be used in server components, server actions, and route handlers (but importantly not middleware). + * @returns The session, or null if it doesn't exist. + */ +export const getCurrentSession = cache( + async (): Promise => { + const token = (await cookies()).get("session")?.value ?? null; + if (token === null) { + return { session: null, user: null }; + } + const result = await validateSessionToken(token); + return result; + } +); + +/** + * Invalidate all sessions for a user by deleting them from the database. + * @param userId - The ID of the user. + */ +export async function invalidateUserSessions(userId: string): Promise { + const sessions = await sessionsLayout.findAll({ + query: { id_user: `==${userId}` }, + }); + for (const session of sessions) { + await sessionsLayout.delete({ recordId: session.recordId }); + } +} + +/** + * Set a cookie for a session. + * @param token - The session token. + * @param expiresAt - The expiration date of the session. + */ +export async function setSessionTokenCookie( + token: string, + expiresAt: Date +): Promise { + (await cookies()).set("session", token, { + httpOnly: true, + path: "/", + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + expires: expiresAt, + }); +} + +/** + * Delete the session cookie. + */ +export async function deleteSessionTokenCookie(): Promise { + (await cookies()).set("session", "", { + httpOnly: true, + path: "/", + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 0, + }); +} + +export interface Session { + id: string; + expiresAt: Date; + id_user: string; +} + +type SessionValidationResult = + | { session: Session; user: User } + | { session: null; user: null }; diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts new file mode 100644 index 00000000..1b7e0194 --- /dev/null +++ b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts @@ -0,0 +1,146 @@ +import { usersLayout } from "../db/client"; +import { Tusers as _User } from "../db/users"; +import { hashPassword, verifyPasswordHash } from "./password"; + +export type User = Partial< + Omit<_User, "id" | "password_hash" | "recovery_code" | "emailVerified"> +> & { + id: string; + email: string; + emailVerified: boolean; +}; + +/** An internal helper function to fetch a user from the database. */ +async function fetchUser(userId: string) { + const { data } = await usersLayout.findOne({ + query: { id: `==${userId}` }, + }); + return data; +} + +/** Create a new user in the database. */ +export async function createUser( + email: string, + password: string +): Promise { + const password_hash = await hashPassword(password); + const { recordId } = await usersLayout.create({ + fieldData: { + email, + password_hash, + emailVerified: 0, + }, + }); + const fmResult = await usersLayout.get({ recordId }); + const { fieldData } = fmResult.data[0]; + + const user: User = { + id: fieldData.id, + email, + emailVerified: false, + username: "", + }; + return user; +} + +/** Update a user's password in the database. */ +export async function updateUserPassword( + userId: string, + password: string +): Promise { + const password_hash = await hashPassword(password); + const { recordId } = await fetchUser(userId); + + await usersLayout.update({ recordId, fieldData: { password_hash } }); +} + +export async function updateUserEmailAndSetEmailAsVerified( + userId: string, + email: string +): Promise { + const { recordId } = await fetchUser(userId); + await usersLayout.update({ + recordId, + fieldData: { email, emailVerified: 1 }, + }); +} + +export async function setUserAsEmailVerifiedIfEmailMatches( + userId: string, + email: string +): Promise { + try { + const { + data: { recordId }, + } = await usersLayout.findOne({ + query: { id: `==${userId}`, email: `==${email}` }, + }); + await usersLayout.update({ recordId, fieldData: { emailVerified: 1 } }); + return true; + } catch (error) { + return false; + } +} + +export async function getUserFromEmail(email: string): Promise { + const fmResult = await usersLayout.maybeFindFirst({ + query: { email: `==${email}` }, + }); + if (fmResult === null) return null; + + const { + data: { fieldData }, + } = fmResult; + + const user: User = { + id: fieldData.id, + email: fieldData.email, + emailVerified: Boolean(fieldData.emailVerified), + username: fieldData.username, + }; + return user; +} + +/** + * Validate a user's email/password combination. + * @param email - The user's email. + * @param password - The user's password. + * @returns The user, or null if the login is invalid. + */ +export async function validateLogin( + email: string, + password: string +): Promise { + try { + const { + data: { fieldData }, + } = await usersLayout.findOne({ + query: { email: `==${email}` }, + }); + + const validPassword = await verifyPasswordHash( + fieldData.password_hash, + password + ); + if (!validPassword) { + return null; + } + const user: User = { + id: fieldData.id, + email: fieldData.email, + emailVerified: Boolean(fieldData.emailVerified), + username: fieldData.username, + }; + return user; + } catch (error) { + return null; + } +} + +export async function checkEmailAvailability(email: string): Promise { + const { data } = await usersLayout.find({ + query: { email: `==${email}` }, + ignoreEmptyResult: true, + }); + return data.length === 0; +} diff --git a/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma b/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma new file mode 100644 index 00000000..6b9dd139 --- /dev/null +++ b/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma @@ -0,0 +1,24 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + + // If you have enabled foreign key constraints for your database, remove this line. + relationMode = "prisma" +} + +model Post { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([name]) +} diff --git a/packages/cli-old/template/extras/prisma/schema/base.prisma b/packages/cli-old/template/extras/prisma/schema/base.prisma new file mode 100644 index 00000000..ddb6e099 --- /dev/null +++ b/packages/cli-old/template/extras/prisma/schema/base.prisma @@ -0,0 +1,20 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model Post { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([name]) +} diff --git a/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma b/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma new file mode 100644 index 00000000..198915b9 --- /dev/null +++ b/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma @@ -0,0 +1,77 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + + // If you have enabled foreign key constraints for your database, remove this line. + relationMode = "prisma" +} + +model Post { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + + @@index([name]) + @@index([createdById]) +} + +// Necessary for Next auth +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([userId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + posts Post[] +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} diff --git a/packages/cli-old/template/extras/prisma/schema/with-auth.prisma b/packages/cli-old/template/extras/prisma/schema/with-auth.prisma new file mode 100644 index 00000000..b17831e6 --- /dev/null +++ b/packages/cli-old/template/extras/prisma/schema/with-auth.prisma @@ -0,0 +1,74 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below + // Further reading: + // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema + // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string + url = env("DATABASE_URL") +} + +model Post { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + + @@index([name]) +} + +// Necessary for Next auth +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? // @db.Text + access_token String? // @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? // @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + refresh_token_expires_in Int? + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + posts Post[] +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} diff --git a/packages/cli-old/template/extras/src/app/_components/post-tw.tsx b/packages/cli-old/template/extras/src/app/_components/post-tw.tsx new file mode 100644 index 00000000..ebe15eab --- /dev/null +++ b/packages/cli-old/template/extras/src/app/_components/post-tw.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState } from "react"; + +import { api } from "~/trpc/react"; + +export function LatestPost() { + const [latestPost] = api.post.getLatest.useSuspenseQuery(); + + const utils = api.useUtils(); + const [name, setName] = useState(""); + const createPost = api.post.create.useMutation({ + onSuccess: async () => { + await utils.post.invalidate(); + setName(""); + }, + }); + + return ( +
+ {latestPost ? ( +

Your most recent post: {latestPost.name}

+ ) : ( +

You have no posts yet.

+ )} +
{ + 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 new file mode 100644 index 00000000..1ad81347 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/_components/post.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useState } from "react"; + +import { api } from "~/trpc/react"; +import styles from "../index.module.css"; + +export function LatestPost() { + const [latestPost] = api.post.getLatest.useSuspenseQuery(); + + const utils = api.useUtils(); + const [name, setName] = useState(""); + const createPost = api.post.create.useMutation({ + onSuccess: async () => { + await utils.post.invalidate(); + setName(""); + }, + }); + + return ( +
+ {latestPost ? ( +

+ Your most recent post: {latestPost.name} +

+ ) : ( +

You have no posts yet.

+ )} + +
{ + 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 new file mode 100644 index 00000000..fbb80152 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,4 @@ +import { handlers } from "@/server/auth"; // Referring to the auth.ts we just created + + +export const { GET, POST } = handlers; diff --git a/packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts b/packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts new file mode 100644 index 00000000..5fbd827d --- /dev/null +++ b/packages/cli-old/template/extras/src/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,34 @@ +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { type NextRequest } from "next/server"; + +import { env } from "~/env"; +import { appRouter } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/trpc"; + +/** + * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when + * handling a HTTP request (e.g. when you make requests from Client Components). + */ +const createContext = async (req: NextRequest) => { + return createTRPCContext({ + headers: req.headers, + }); +}; + +const handler = (req: NextRequest) => + fetchRequestHandler({ + endpoint: "/api/trpc", + req, + router: appRouter, + createContext: () => createContext(req), + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error( + `❌ tRPC failed on ${path ?? ""}: ${error.message}` + ); + } + : undefined, + }); + +export { handler as GET, handler as POST }; diff --git a/packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx b/packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx new file mode 100644 index 00000000..4382acb8 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/clerk-auth/layout.tsx @@ -0,0 +1,10 @@ +import { Center } from "@mantine/core"; +import React from "react"; + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx b/packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx new file mode 100644 index 00000000..2cc13d4c --- /dev/null +++ b/packages/cli-old/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx @@ -0,0 +1,5 @@ +import { SignIn } from "@clerk/nextjs"; + +export default function Page() { + return ; +} diff --git a/packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx b/packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx new file mode 100644 index 00000000..27439454 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx @@ -0,0 +1,5 @@ +import { SignUp } from "@clerk/nextjs"; + +export default function Page() { + return ; +} diff --git a/packages/cli-old/template/extras/src/app/layout/base.tsx b/packages/cli-old/template/extras/src/app/layout/base.tsx new file mode 100644 index 00000000..e0382db7 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/layout/base.tsx @@ -0,0 +1,34 @@ +import { ColorSchemeScript, MantineProvider } from "@mantine/core"; +import { ModalsProvider } from "@mantine/modals"; +import { Notifications } from "@mantine/notifications"; + +import "@mantine/core/styles.css"; +import "@mantine/notifications/styles.css"; +import "@mantine/dates/styles.css"; +import "mantine-react-table/styles.css"; + +import { type Metadata } from "next"; + +export const metadata: Metadata = { + title: "My ProofKit App", + description: "Generated by proofkit", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + + + + + + {children} + + + + ); +} diff --git a/packages/cli-old/template/extras/src/app/layout/main-shell.tsx b/packages/cli-old/template/extras/src/app/layout/main-shell.tsx new file mode 100644 index 00000000..77fa7adf --- /dev/null +++ b/packages/cli-old/template/extras/src/app/layout/main-shell.tsx @@ -0,0 +1,37 @@ +import { + AppShell, + AppShellFooter, + AppShellHeader, + AppShellMain, + AppShellNavbar, +} from "@mantine/core"; +import React from "react"; + +/** Layout configuration Edit these values to change the layout */ +export const showHeader = false; +export const showFooter = false; +export const showLeftNavbar = false; + +export const headerHeight = 60; +export const footerHeight = 60; +export const leftNavbarWidth = 200; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {showHeader && Header} + {showLeftNavbar && Left Navbar} + {children} + {showFooter && Footer} + + ); +} diff --git a/packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx b/packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx new file mode 100644 index 00000000..c1218810 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/layout/with-trpc-tw.tsx @@ -0,0 +1,24 @@ +import "~/styles/globals.css"; + +import { GeistSans } from "geist/font/sans"; +import { type Metadata } from "next"; + +import { TRPCReactProvider } from "~/trpc/react"; + +export const metadata: Metadata = { + title: "Create T3 App", + description: "Generated by proofkit", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + ); +} diff --git a/packages/cli-old/template/extras/src/app/layout/with-trpc.tsx b/packages/cli-old/template/extras/src/app/layout/with-trpc.tsx new file mode 100644 index 00000000..6471a2ae --- /dev/null +++ b/packages/cli-old/template/extras/src/app/layout/with-trpc.tsx @@ -0,0 +1,24 @@ +import "~/styles/globals.css"; + +import { GeistSans } from "geist/font/sans"; +import { type Metadata } from "next"; + +import { TRPCReactProvider } from "~/trpc/react"; + +export const metadata: Metadata = { + title: "Create T3 App", + description: "Generated by proofkit", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + ); +} diff --git a/packages/cli-old/template/extras/src/app/layout/with-tw.tsx b/packages/cli-old/template/extras/src/app/layout/with-tw.tsx new file mode 100644 index 00000000..5dea6caf --- /dev/null +++ b/packages/cli-old/template/extras/src/app/layout/with-tw.tsx @@ -0,0 +1,20 @@ +import "~/styles/globals.css"; + +import { GeistSans } from "geist/font/sans"; +import { type Metadata } from "next"; + +export const metadata: Metadata = { + title: "Create T3 App", + description: "Generated by proofkit", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/packages/cli-old/template/extras/src/app/next-auth/layout.tsx b/packages/cli-old/template/extras/src/app/next-auth/layout.tsx new file mode 100644 index 00000000..51933f24 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/next-auth/layout.tsx @@ -0,0 +1,22 @@ +import { auth } from "@/server/auth"; +import { Card, Center } from "@mantine/core"; +import { redirect } from "next/navigation"; +import React from "react"; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + if (session) { + return redirect("/"); + } + return ( +
+ + {children} + +
+ ); +} diff --git a/packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx b/packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx new file mode 100644 index 00000000..b4781c4d --- /dev/null +++ b/packages/cli-old/template/extras/src/app/next-auth/signin/page.tsx @@ -0,0 +1,83 @@ +import { providerMap, signIn } from "@/server/auth"; +import { + Button, + Card, + Divider, + PasswordInput, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { AuthError } from "next-auth"; +import Link from "next/link"; +import { redirect } from "next/navigation"; + +export default async function SignInPage(props: { + searchParams: Promise<{ callbackUrl: string | undefined }>; +}) { + const searchParams = await props.searchParams; + return ( + +
{ + "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 new file mode 100644 index 00000000..fba6508d --- /dev/null +++ b/packages/cli-old/template/extras/src/app/next-auth/signup/action.ts @@ -0,0 +1,24 @@ +"use server"; + +import { signIn } from "@/server/auth"; +import { userSignUp } from "@/server/data/users"; +import { actionClient } from "@/server/safe-action"; + +import { signUpSchema } from "./validation"; + +export const signUpAction = actionClient + .schema(signUpSchema) + .action(async ({ parsedInput, ctx }) => { + const { email, password } = parsedInput; + + await userSignUp({ email, password }); + + await signIn("credentials", { + email, + password, + }); + + return { + success: true, + }; + }); diff --git a/packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx b/packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx new file mode 100644 index 00000000..faab245f --- /dev/null +++ b/packages/cli-old/template/extras/src/app/next-auth/signup/page.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button, PasswordInput, Stack, Text, TextInput } from "@mantine/core"; +import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; +import Link from "next/link"; +import React from "react"; + +import { signUpAction } from "./action"; +import { signUpSchema } from "./validation"; + +export default function SignUpPage(props: { + searchParams: Promise<{ callbackUrl: string | undefined }>; +}) { + const { form, action, handleSubmitWithAction, resetFormAndAction } = + useHookFormAction(signUpAction, zodResolver(signUpSchema), { + actionProps: {}, + formProps: {}, + errorMapProps: {}, + }); + + return ( + +
+ + + + + + +
+ + Already have an account? Sign in + +
+ ); +} diff --git a/packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts b/packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts new file mode 100644 index 00000000..d3086d30 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/next-auth/signup/validation.ts @@ -0,0 +1,12 @@ +import { z } from "zod/v4"; + +export const signUpSchema = z + .object({ + email: z.string().email(), + password: z.string(), + passwordConfirm: z.string(), + }) + .refine((data) => data.password === data.passwordConfirm, { + message: "Passwords don't match", + path: ["passwordConfirm"], + }); diff --git a/packages/cli-old/template/extras/src/app/page/base.tsx b/packages/cli-old/template/extras/src/app/page/base.tsx new file mode 100644 index 00000000..bf905890 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/page/base.tsx @@ -0,0 +1,6 @@ +import { Text } from "@mantine/core"; +import Link from "next/link"; + +export default function Home() { + return Welcome!; +} diff --git a/packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx b/packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx new file mode 100644 index 00000000..49c9bbbe --- /dev/null +++ b/packages/cli-old/template/extras/src/app/page/with-auth-trpc-tw.tsx @@ -0,0 +1,67 @@ +import Link from "next/link"; + +import { LatestPost } from "~/app/_components/post"; +import { getServerAuthSession } from "~/server/auth"; +import { api, HydrateClient } from "~/trpc/server"; + +export default async function Home() { + const hello = await api.post.hello({ text: "from tRPC" }); + const session = await getServerAuthSession(); + + void api.post.getLatest.prefetch(); + + return ( + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello ? hello.greeting : "Loading tRPC query..."} +

+ +
+

+ {session && Logged in as {session.user?.name}} +

+ + {session ? "Sign out" : "Sign in"} + +
+
+ + {session?.user && } +
+
+
+ ); +} diff --git a/packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx b/packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx new file mode 100644 index 00000000..cfeed2f5 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/page/with-auth-trpc.tsx @@ -0,0 +1,68 @@ +import Link from "next/link"; + +import { LatestPost } from "~/app/_components/post"; +import { getServerAuthSession } from "~/server/auth"; +import { api, HydrateClient } from "~/trpc/server"; +import styles from "./index.module.css"; + +export default async function Home() { + const hello = await api.post.hello({ text: "from tRPC" }); + const session = await getServerAuthSession(); + + void api.post.getLatest.prefetch(); + + return ( + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello ? hello.greeting : "Loading tRPC query..."} +

+ +
+

+ {session && Logged in as {session.user?.name}} +

+ + {session ? "Sign out" : "Sign in"} + +
+
+ + {session?.user && } +
+
+
+ ); +} diff --git a/packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx b/packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx new file mode 100644 index 00000000..d7121d83 --- /dev/null +++ b/packages/cli-old/template/extras/src/app/page/with-trpc-tw.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; + +import { LatestPost } from "~/app/_components/post"; +import { api, HydrateClient } from "~/trpc/server"; + +export default async function Home() { + const hello = await api.post.hello({ text: "from tRPC" }); + + void api.post.getLatest.prefetch(); + + return ( + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello ? hello.greeting : "Loading tRPC query..."} +

+
+ + +
+
+
+ ); +} diff --git a/packages/cli-old/template/extras/src/app/page/with-trpc.tsx b/packages/cli-old/template/extras/src/app/page/with-trpc.tsx new file mode 100644 index 00000000..035f250b --- /dev/null +++ b/packages/cli-old/template/extras/src/app/page/with-trpc.tsx @@ -0,0 +1,54 @@ +import Link from "next/link"; + +import { LatestPost } from "~/app/_components/post"; +import { api, HydrateClient } from "~/trpc/server"; +import styles from "./index.module.css"; + +export default async function Home() { + const hello = await api.post.hello({ text: "from tRPC" }); + + void api.post.getLatest.prefetch(); + + return ( + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello ? hello.greeting : "Loading tRPC query..."} +

+
+ + +
+
+
+ ); +} diff --git a/packages/cli-old/template/extras/src/app/page/with-tw.tsx b/packages/cli-old/template/extras/src/app/page/with-tw.tsx new file mode 100644 index 00000000..773fef1b --- /dev/null +++ b/packages/cli-old/template/extras/src/app/page/with-tw.tsx @@ -0,0 +1,37 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to + deploy it. +
+ +
+
+
+ ); +} diff --git a/packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx b/packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx new file mode 100644 index 00000000..50e2f512 --- /dev/null +++ b/packages/cli-old/template/extras/src/components/clerk-auth/clerk-provider.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { ClerkProvider } from "@clerk/nextjs"; +import { dark } from "@clerk/themes"; +import { useComputedColorScheme } from "@mantine/core"; + +export function ClerkAuthProvider({ children }: { children: React.ReactNode }) { + const computedColorScheme = useComputedColorScheme(); + return ( + + {children} + + ); +} diff --git a/packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx b/packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx new file mode 100644 index 00000000..33683736 --- /dev/null +++ b/packages/cli-old/template/extras/src/components/clerk-auth/user-menu-mobile.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useClerk, useUser } from "@clerk/nextjs"; +import { Menu } from "@mantine/core"; +import { useRouter } from "next/navigation"; +import React from "react"; + +/** + * Shown in the mobile header menu + */ +export default function UserMenuMobile() { + const { isSignedIn, isLoaded, user } = useUser(); + const { signOut, buildSignInUrl } = useClerk(); + const router = useRouter(); + + if (!isLoaded) return null; + + if (!isSignedIn) + return ( + <> + + router.push(buildSignInUrl())}> + Sign In + + + ); + + if (isSignedIn) + return ( + <> + + {user.primaryEmailAddress?.emailAddress} + signOut()}>Sign Out + + ); +} diff --git a/packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx b/packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx new file mode 100644 index 00000000..6f8da571 --- /dev/null +++ b/packages/cli-old/template/extras/src/components/clerk-auth/user-menu.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useClerk, UserButton, useUser } from "@clerk/nextjs"; +import { Button } from "@mantine/core"; +import { useRouter } from "next/navigation"; + +export default function UserMenu() { + const { isSignedIn, isLoaded } = useUser(); + const { buildSignInUrl } = useClerk(); + const router = useRouter(); + + if (!isLoaded) return null; + + if (!isSignedIn) + return ( + + ); + + if (isSignedIn) return ; + + return null; +} diff --git a/packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx b/packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx new file mode 100644 index 00000000..e6f328f4 --- /dev/null +++ b/packages/cli-old/template/extras/src/components/next-auth/next-auth-provider.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { Session } from "next-auth"; +import { SessionProvider } from "next-auth/react"; + +export function NextAuthProvider({ + children, + session, +}: { + children: React.ReactNode; + session: Session | null | undefined; +}) { + return {children}; +} diff --git a/packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx b/packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx new file mode 100644 index 00000000..5cadae53 --- /dev/null +++ b/packages/cli-old/template/extras/src/components/next-auth/user-menu-mobile.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { Menu } from "@mantine/core"; +import { signIn, signOut, useSession } from "next-auth/react"; +import React from "react"; + +/** + * Shown in the mobile header menu + */ +export default function UserMenuMobile() { + const { data: session, status } = useSession(); + + if (status === "loading") return null; + + if (status === "unauthenticated") + return ( + <> + + signIn()}>Sign In + + ); + + if (status === "authenticated") + return ( + <> + + {session.user.email} + signOut()}>Sign Out + + ); +} diff --git a/packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx b/packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx new file mode 100644 index 00000000..a1305c5d --- /dev/null +++ b/packages/cli-old/template/extras/src/components/next-auth/user-menu.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Button, Menu, px } from "@mantine/core"; +import { IconChevronDown } from "@tabler/icons-react"; +import { signIn, signOut, useSession } from "next-auth/react"; + +export default function UserMenu() { + const { data: session, status } = useSession(); + + if (status === "loading") return null; + + if (status === "unauthenticated") + return ( + + ); + + if (status === "authenticated") + return ( + + + + + + 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 new file mode 100644 index 00000000..e73bf132 --- /dev/null +++ b/packages/cli-old/template/extras/src/env/with-auth.ts @@ -0,0 +1,31 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod/v4"; + +export const env = createEnv({ + server: { + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), + FM_DATABASE: z.string().endsWith(".fmp12"), + FM_SERVER: z.string().url(), + OTTO_API_KEY: z.string().startsWith("dk_"), + + // Next Auth + NEXTAUTH_SECRET: + process.env.NODE_ENV === "production" + ? z.string() + : z.string().optional(), + NEXTAUTH_URL: z.preprocess( + // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL + // Since NextAuth.js automatically uses the VERCEL_URL if present. + (str) => process.env.VERCEL_URL ?? str, + // VERCEL_URL doesn't include `https` so it cant be validated as a URL + process.env.VERCEL ? z.string() : z.string().url() + ), + DISCORD_CLIENT_ID: z.string(), + DISCORD_CLIENT_SECRET: z.string(), + }, + client: {}, + // For Next.js >= 13.4.4, you only need to destructure client variables: + experimental__runtimeEnv: {}, +}); diff --git a/packages/cli-old/template/extras/src/env/with-clerk.ts b/packages/cli-old/template/extras/src/env/with-clerk.ts new file mode 100644 index 00000000..e9825af7 --- /dev/null +++ b/packages/cli-old/template/extras/src/env/with-clerk.ts @@ -0,0 +1,20 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod/v4"; + +export const env = createEnv({ + server: { + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), + FM_DATABASE: z.string().endsWith(".fmp12"), + FM_SERVER: z.string().url(), + OTTO_API_KEY: z.string().startsWith("dk_"), + + // Clerk + CLERK_SECRET_KEY: z.string().min(1), + CLERK_WEBHOOK_SECRET: z.string().min(1), + }, + client: {}, + // For Next.js >= 13.4.4, you only need to destructure client variables: + experimental__runtimeEnv: {}, +}); diff --git a/packages/cli-old/template/extras/src/index.module.css b/packages/cli-old/template/extras/src/index.module.css new file mode 100644 index 00000000..fac9982a --- /dev/null +++ b/packages/cli-old/template/extras/src/index.module.css @@ -0,0 +1,177 @@ +.main { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + background-image: linear-gradient(to bottom, #2e026d, #15162c); +} + +.container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3rem; + padding: 4rem 1rem; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.title { + font-size: 3rem; + line-height: 1; + font-weight: 800; + letter-spacing: -0.025em; + margin: 0; + color: white; +} + +@media (min-width: 640px) { + .title { + font-size: 5rem; + } +} + +.pinkSpan { + color: hsl(280 100% 70%); +} + +.cardRow { + display: grid; + grid-template-columns: repeat(1, minmax(0, 1fr)); + gap: 1rem; +} + +@media (min-width: 640px) { + .cardRow { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 768px) { + .cardRow { + gap: 2rem; + } +} + +.card { + max-width: 20rem; + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + border-radius: 0.75rem; + color: white; + background-color: rgb(255 255 255 / 0.1); +} + +.card:hover { + background-color: rgb(255 255 255 / 0.2); + transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); +} + +.cardTitle { + font-size: 1.5rem; + line-height: 2rem; + font-weight: 700; + margin: 0; +} + +.cardText { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.showcaseContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.showcaseText { + color: white; + text-align: center; + font-size: 1.5rem; + line-height: 2rem; +} + +.authContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; +} + +.loginButton { + border-radius: 9999px; + background-color: rgb(255 255 255 / 0.1); + padding: 0.75rem 2.5rem; + font-weight: 600; + color: white; + text-decoration-line: none; + transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); +} + +.loginButton:hover { + background-color: rgb(255 255 255 / 0.2); +} + +.form { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.input { + width: 100%; + border-radius: 9999px; + padding: 0.5rem 1rem; + color: black; +} + +.submitButton { + all: unset; + border-radius: 9999px; + background-color: rgb(255 255 255 / 0.1); + padding: 0.75rem 2.5rem; + font-weight: 600; + color: white; + text-align: center; + transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); +} + +.submitButton:hover { + background-color: rgb(255 255 255 / 0.2); +} diff --git a/packages/cli-old/template/extras/src/middleware/clerk.ts b/packages/cli-old/template/extras/src/middleware/clerk.ts new file mode 100644 index 00000000..1dd75bb4 --- /dev/null +++ b/packages/cli-old/template/extras/src/middleware/clerk.ts @@ -0,0 +1,20 @@ +import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; + +// these default settings will require authentication for all routes except the ones in the array +// to restrict public access to the home page, remove "/" from the array +const isPublicRoute = createRouteMatcher(["/auth/(.*)", "/"]); + +export default clerkMiddleware(async (auth, request) => { + if (!isPublicRoute(request)) { + await auth.protect(); + } +}); + +export const config = { + matcher: [ + // Skip Next.js internals and all static files, unless found in search params + "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", + // Always run for API routes + "/(api|trpc)(.*)", + ], +}; diff --git a/packages/cli-old/template/extras/src/middleware/next-auth.ts b/packages/cli-old/template/extras/src/middleware/next-auth.ts new file mode 100644 index 00000000..e1f450d4 --- /dev/null +++ b/packages/cli-old/template/extras/src/middleware/next-auth.ts @@ -0,0 +1,5 @@ +export { auth as middleware } from "@/server/auth"; + +export const config = { + matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], +}; diff --git a/packages/cli-old/template/extras/src/pages/_app/base.tsx b/packages/cli-old/template/extras/src/pages/_app/base.tsx new file mode 100644 index 00000000..e7e7fb29 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/_app/base.tsx @@ -0,0 +1,14 @@ +import { GeistSans } from "geist/font/sans"; +import { type AppType } from "next/dist/shared/lib/utils"; + +import "~/styles/globals.css"; + +const MyApp: AppType = ({ Component, pageProps }) => { + return ( +
+ +
+ ); +}; + +export default MyApp; diff --git a/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx b/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx new file mode 100644 index 00000000..89d10b0c --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc-tw.tsx @@ -0,0 +1,23 @@ +import { GeistSans } from "geist/font/sans"; +import { type Session } from "next-auth"; +import { SessionProvider } from "next-auth/react"; +import { type AppType } from "next/app"; + +import { api } from "~/utils/api"; + +import "~/styles/globals.css"; + +const MyApp: AppType<{ session: Session | null }> = ({ + Component, + pageProps: { session, ...pageProps }, +}) => { + return ( + +
+ +
+
+ ); +}; + +export default api.withTRPC(MyApp); diff --git a/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx b/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx new file mode 100644 index 00000000..89d10b0c --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/_app/with-auth-trpc.tsx @@ -0,0 +1,23 @@ +import { GeistSans } from "geist/font/sans"; +import { type Session } from "next-auth"; +import { SessionProvider } from "next-auth/react"; +import { type AppType } from "next/app"; + +import { api } from "~/utils/api"; + +import "~/styles/globals.css"; + +const MyApp: AppType<{ session: Session | null }> = ({ + Component, + pageProps: { session, ...pageProps }, +}) => { + return ( + +
+ +
+
+ ); +}; + +export default api.withTRPC(MyApp); diff --git a/packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx b/packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx new file mode 100644 index 00000000..a008ed16 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/_app/with-auth-tw.tsx @@ -0,0 +1,21 @@ +import { GeistSans } from "geist/font/sans"; +import { type Session } from "next-auth"; +import { SessionProvider } from "next-auth/react"; +import { type AppType } from "next/app"; + +import "~/styles/globals.css"; + +const MyApp: AppType<{ session: Session | null }> = ({ + Component, + pageProps: { session, ...pageProps }, +}) => { + return ( + +
+ +
+
+ ); +}; + +export default MyApp; diff --git a/packages/cli-old/template/extras/src/pages/_app/with-auth.tsx b/packages/cli-old/template/extras/src/pages/_app/with-auth.tsx new file mode 100644 index 00000000..a008ed16 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/_app/with-auth.tsx @@ -0,0 +1,21 @@ +import { GeistSans } from "geist/font/sans"; +import { type Session } from "next-auth"; +import { SessionProvider } from "next-auth/react"; +import { type AppType } from "next/app"; + +import "~/styles/globals.css"; + +const MyApp: AppType<{ session: Session | null }> = ({ + Component, + pageProps: { session, ...pageProps }, +}) => { + return ( + +
+ +
+
+ ); +}; + +export default MyApp; diff --git a/packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx b/packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx new file mode 100644 index 00000000..464c50cc --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/_app/with-trpc-tw.tsx @@ -0,0 +1,16 @@ +import { GeistSans } from "geist/font/sans"; +import { type AppType } from "next/app"; + +import { api } from "~/utils/api"; + +import "~/styles/globals.css"; + +const MyApp: AppType = ({ Component, pageProps }) => { + return ( +
+ +
+ ); +}; + +export default api.withTRPC(MyApp); diff --git a/packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx b/packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx new file mode 100644 index 00000000..464c50cc --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/_app/with-trpc.tsx @@ -0,0 +1,16 @@ +import { GeistSans } from "geist/font/sans"; +import { type AppType } from "next/app"; + +import { api } from "~/utils/api"; + +import "~/styles/globals.css"; + +const MyApp: AppType = ({ Component, pageProps }) => { + return ( +
+ +
+ ); +}; + +export default api.withTRPC(MyApp); diff --git a/packages/cli-old/template/extras/src/pages/_app/with-tw.tsx b/packages/cli-old/template/extras/src/pages/_app/with-tw.tsx new file mode 100644 index 00000000..da39269a --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/_app/with-tw.tsx @@ -0,0 +1,14 @@ +import { GeistSans } from "geist/font/sans"; +import { type AppType } from "next/app"; + +import "~/styles/globals.css"; + +const MyApp: AppType = ({ Component, pageProps }) => { + return ( +
+ +
+ ); +}; + +export default MyApp; diff --git a/packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts b/packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts new file mode 100644 index 00000000..8739530f --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/api/auth/[...nextauth].ts @@ -0,0 +1,5 @@ +import NextAuth from "next-auth"; + +import { authOptions } from "~/server/auth"; + +export default NextAuth(authOptions); diff --git a/packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts b/packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts new file mode 100644 index 00000000..587dd2bd --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/api/trpc/[trpc].ts @@ -0,0 +1,19 @@ +import { createNextApiHandler } from "@trpc/server/adapters/next"; + +import { env } from "~/env"; +import { appRouter } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/trpc"; + +// export API handler +export default createNextApiHandler({ + router: appRouter, + createContext: createTRPCContext, + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error( + `❌ tRPC failed on ${path ?? ""}: ${error.message}` + ); + } + : undefined, +}); diff --git a/packages/cli-old/template/extras/src/pages/index/base.tsx b/packages/cli-old/template/extras/src/pages/index/base.tsx new file mode 100644 index 00000000..a34888c6 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/index/base.tsx @@ -0,0 +1,47 @@ +import Head from "next/head"; +import Link from "next/link"; + +import styles from "./index.module.css"; + +export default function Home() { + return ( + <> + + Create T3 App + + + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+
+ + ); +} diff --git a/packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx b/packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx new file mode 100644 index 00000000..532e7f73 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/index/with-auth-trpc-tw.tsx @@ -0,0 +1,80 @@ +import { signIn, signOut, useSession } from "next-auth/react"; +import Head from "next/head"; +import Link from "next/link"; + +import { api } from "~/utils/api"; + +export default function Home() { + const hello = api.post.hello.useQuery({ text: "from tRPC" }); + + return ( + <> + + Create T3 App + + + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello.data ? hello.data.greeting : "Loading tRPC query..."} +

+ +
+
+
+ + ); +} + +function AuthShowcase() { + const { data: sessionData } = useSession(); + + const { data: secretMessage } = api.post.getSecretMessage.useQuery( + undefined, // no input + { enabled: sessionData?.user !== undefined } + ); + + return ( +
+

+ {sessionData && Logged in as {sessionData.user?.name}} + {secretMessage && - {secretMessage}} +

+ +
+ ); +} diff --git a/packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx b/packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx new file mode 100644 index 00000000..f3191246 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/index/with-auth-trpc.tsx @@ -0,0 +1,81 @@ +import { signIn, signOut, useSession } from "next-auth/react"; +import Head from "next/head"; +import Link from "next/link"; + +import { api } from "~/utils/api"; +import styles from "./index.module.css"; + +export default function Home() { + const hello = api.post.hello.useQuery({ text: "from tRPC" }); + + return ( + <> + + Create T3 App + + + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello.data ? hello.data.greeting : "Loading tRPC query..."} +

+ +
+
+
+ + ); +} + +function AuthShowcase() { + const { data: sessionData } = useSession(); + + const { data: secretMessage } = api.post.getSecretMessage.useQuery( + undefined, // no input + { enabled: sessionData?.user !== undefined } + ); + + return ( +
+

+ {sessionData && Logged in as {sessionData.user?.name}} + {secretMessage && - {secretMessage}} +

+ +
+ ); +} diff --git a/packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx b/packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx new file mode 100644 index 00000000..3a51c3e8 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/index/with-trpc-tw.tsx @@ -0,0 +1,52 @@ +import Head from "next/head"; +import Link from "next/link"; + +import { api } from "~/utils/api"; + +export default function Home() { + const hello = api.post.hello.useQuery({ text: "from tRPC" }); + + return ( + <> + + Create T3 App + + + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+

+ {hello.data ? hello.data.greeting : "Loading tRPC query..."} +

+
+
+ + ); +} diff --git a/packages/cli-old/template/extras/src/pages/index/with-trpc.tsx b/packages/cli-old/template/extras/src/pages/index/with-trpc.tsx new file mode 100644 index 00000000..26d807f9 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/index/with-trpc.tsx @@ -0,0 +1,53 @@ +import Head from "next/head"; +import Link from "next/link"; + +import { api } from "~/utils/api"; +import styles from "./index.module.css"; + +export default function Home() { + const hello = api.post.hello.useQuery({ text: "from tRPC" }); + + return ( + <> + + Create T3 App + + + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+

+ {hello.data ? hello.data.greeting : "Loading tRPC query..."} +

+
+
+ + ); +} diff --git a/packages/cli-old/template/extras/src/pages/index/with-tw.tsx b/packages/cli-old/template/extras/src/pages/index/with-tw.tsx new file mode 100644 index 00000000..88b818e2 --- /dev/null +++ b/packages/cli-old/template/extras/src/pages/index/with-tw.tsx @@ -0,0 +1,45 @@ +import Head from "next/head"; +import Link from "next/link"; + +export default function Home() { + return ( + <> + + Create T3 App + + + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+
+ + ); +} diff --git a/packages/cli-old/template/extras/src/server/api/root.ts b/packages/cli-old/template/extras/src/server/api/root.ts new file mode 100644 index 00000000..b341fc4d --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/root.ts @@ -0,0 +1,23 @@ +import { postRouter } from "~/server/api/routers/post"; +import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; + +/** + * This is the primary router for your server. + * + * All routers added in /api/routers should be manually added here. + */ +export const appRouter = createTRPCRouter({ + post: postRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; + +/** + * Create a server-side caller for the tRPC API. + * @example + * const trpc = createCaller(createContext); + * const res = await trpc.post.all(); + * ^? Post[] + */ +export const createCaller = createCallerFactory(appRouter); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/base.ts b/packages/cli-old/template/extras/src/server/api/routers/post/base.ts new file mode 100644 index 00000000..6781c531 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/routers/post/base.ts @@ -0,0 +1,40 @@ +import { z } from "zod/v4"; + +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; + +// Mocked DB +interface Post { + id: number; + name: string; +} +const posts: Post[] = [ + { + id: 1, + name: "Hello World", + }, +]; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ input }) => { + const post: Post = { + id: posts.length + 1, + name: input.name, + }; + posts.push(post); + return post; + }), + + getLatest: publicProcedure.query(() => { + return posts.at(-1) ?? null; + }), +}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts new file mode 100644 index 00000000..35ac7ba8 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts @@ -0,0 +1,39 @@ +import { z } from "zod/v4"; + +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; +import { posts } from "~/server/db/schema"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + await ctx.db.insert(posts).values({ + name: input.name, + createdById: ctx.session.user.id, + }); + }), + + getLatest: publicProcedure.query(async ({ ctx }) => { + const post = await ctx.db.query.posts.findFirst({ + orderBy: (posts, { desc }) => [desc(posts.createdAt)], + }); + + return post ?? null; + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts new file mode 100644 index 00000000..f0140b76 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts @@ -0,0 +1,41 @@ +import { z } from "zod/v4"; + +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + return ctx.db.post.create({ + data: { + name: input.name, + createdBy: { connect: { id: ctx.session.user.id } }, + }, + }); + }), + + getLatest: protectedProcedure.query(async ({ ctx }) => { + const post = await ctx.db.post.findFirst({ + orderBy: { createdAt: "desc" }, + where: { createdBy: { id: ctx.session.user.id } }, + }); + + return post ?? null; + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts new file mode 100644 index 00000000..8ea389bf --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts @@ -0,0 +1,37 @@ +import { z } from "zod/v4"; + +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; + +let post = { + id: 1, + name: "Hello World", +}; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ input }) => { + post = { id: post.id + 1, name: input.name }; + return post; + }), + + getLatest: protectedProcedure.query(() => { + return post; + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts new file mode 100644 index 00000000..a295842c --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts @@ -0,0 +1,30 @@ +import { z } from "zod/v4"; + +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { posts } from "~/server/db/schema"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + await ctx.db.insert(posts).values({ + name: input.name, + }); + }), + + getLatest: publicProcedure.query(async ({ ctx }) => { + const post = await ctx.db.query.posts.findFirst({ + orderBy: (posts, { desc }) => [desc(posts.createdAt)], + }); + + return post ?? null; + }), +}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts new file mode 100644 index 00000000..3282fa6e --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts @@ -0,0 +1,31 @@ +import { z } from "zod/v4"; + +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + return ctx.db.post.create({ + data: { + name: input.name, + }, + }); + }), + + getLatest: publicProcedure.query(async ({ ctx }) => { + const post = await ctx.db.post.findFirst({ + orderBy: { createdAt: "desc" }, + }); + + return post ?? null; + }), +}); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts new file mode 100644 index 00000000..e831d1a8 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts @@ -0,0 +1,103 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC } from "@trpc/server"; +import superjson from "superjson"; +import { ZodError } from "zod/v4"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + * + * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each + * wrap this and provides the required context. + * + * @see https://trpc.io/docs/server/context + */ +export const createTRPCContext = async (opts: { headers: Headers }) => { + return { + ...opts, + }; +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts new file mode 100644 index 00000000..a8ca5724 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts @@ -0,0 +1,133 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ + +import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; +import { ZodError } from "zod/v4"; + +import { getServerAuthSession } from "~/server/auth"; +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + * + * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each + * wrap this and provides the required context. + * + * @see https://trpc.io/docs/server/context + */ +export const createTRPCContext = async (opts: { headers: Headers }) => { + const session = await getServerAuthSession(); + + return { + db, + session, + ...opts, + }; +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); + }); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts new file mode 100644 index 00000000..55e20cf1 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts @@ -0,0 +1,130 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; +import { ZodError } from "zod/v4"; + +import { getServerAuthSession } from "~/server/auth"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + * + * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each + * wrap this and provides the required context. + * + * @see https://trpc.io/docs/server/context + */ +export const createTRPCContext = async (opts: { headers: Headers }) => { + const session = await getServerAuthSession(); + + return { + session, + ...opts, + }; +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); + }); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts new file mode 100644 index 00000000..c77a7530 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts @@ -0,0 +1,106 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC } from "@trpc/server"; +import superjson from "superjson"; +import { ZodError } from "zod/v4"; + +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + * + * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each + * wrap this and provides the required context. + * + * @see https://trpc.io/docs/server/context + */ +export const createTRPCContext = async (opts: { headers: Headers }) => { + return { + db, + ...opts, + }; +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts new file mode 100644 index 00000000..b1d9f34c --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts @@ -0,0 +1,122 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ + +import { initTRPC } from "@trpc/server"; +import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; +import superjson from "superjson"; +import { ZodError } from "zod/v4"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +type CreateContextOptions = Record; + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +const createInnerTRPCContext = (_opts: CreateContextOptions) => { + return {}; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = (_opts: CreateNextContextOptions) => { + return createInnerTRPCContext({}); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts new file mode 100644 index 00000000..f057f427 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts @@ -0,0 +1,160 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ + +import { initTRPC, TRPCError } from "@trpc/server"; +import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; +import { type Session } from "next-auth"; +import superjson from "superjson"; +import { ZodError } from "zod/v4"; + +import { getServerAuthSession } from "~/server/auth"; +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + session: Session | null; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +const createInnerTRPCContext = (opts: CreateContextOptions) => { + return { + session: opts.session, + db, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async (opts: CreateNextContextOptions) => { + const { req, res } = opts; + + // Get the session from the server using the getServerSession wrapper function + const session = await getServerAuthSession({ req, res }); + + return createInnerTRPCContext({ + session, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); + }); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts new file mode 100644 index 00000000..87a6b0e2 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts @@ -0,0 +1,158 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC, TRPCError } from "@trpc/server"; +import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; +import { type Session } from "next-auth"; +import superjson from "superjson"; +import { ZodError } from "zod/v4"; + +import { getServerAuthSession } from "~/server/auth"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + session: Session | null; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +const createInnerTRPCContext = ({ session }: CreateContextOptions) => { + return { + session, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async ({ + req, + res, +}: CreateNextContextOptions) => { + // Get the session from the server using the getServerSession wrapper function + const session = await getServerAuthSession({ req, res }); + + return createInnerTRPCContext({ + session, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); + }); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts new file mode 100644 index 00000000..a6e5bef7 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts @@ -0,0 +1,125 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC } from "@trpc/server"; +import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; +import superjson from "superjson"; +import { ZodError } from "zod/v4"; + +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +type CreateContextOptions = Record; + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +const createInnerTRPCContext = (_opts: CreateContextOptions) => { + return { + db, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = (_opts: CreateNextContextOptions) => { + return createInnerTRPCContext({}); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli-old/template/extras/src/server/data/users.ts b/packages/cli-old/template/extras/src/server/data/users.ts new file mode 100644 index 00000000..fac1dc35 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/data/users.ts @@ -0,0 +1,23 @@ +import "server-only"; + +import { fmAdapter } from "../auth"; +import { saltAndHashPassword } from "../password"; + +type UserSignUpInput = { + email: string; + password: string; +}; + +export async function userSignUp(input: UserSignUpInput) { + const passwordHash = await saltAndHashPassword(input.password); + + // create the user in our database + const user = await fmAdapter.typedClients.userWithPasswordHash.create({ + fieldData: { + email: input.email, + passwordHash, + }, + }); + + return user; +} diff --git a/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts b/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts new file mode 100644 index 00000000..52188938 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts @@ -0,0 +1,22 @@ +import { Client } from "@planetscale/database"; +import { PrismaPlanetScale } from "@prisma/adapter-planetscale"; +import { PrismaClient } from "@prisma/client"; + +import { env } from "~/env"; + +const psClient = new Client({ url: env.DATABASE_URL }); + +const createPrismaClient = () => + new PrismaClient({ + log: + env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], + adapter: new PrismaPlanetScale(psClient), + }); + +const globalForPrisma = globalThis as unknown as { + prisma: ReturnType | undefined; +}; + +export const db = globalForPrisma.prisma ?? createPrismaClient(); + +if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/cli-old/template/extras/src/server/db/db-prisma.ts b/packages/cli-old/template/extras/src/server/db/db-prisma.ts new file mode 100644 index 00000000..07dc0271 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/db-prisma.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from "@prisma/client"; + +import { env } from "~/env"; + +const createPrismaClient = () => + new PrismaClient({ + log: + env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], + }); + +const globalForPrisma = globalThis as unknown as { + prisma: ReturnType | undefined; +}; + +export const db = globalForPrisma.prisma ?? createPrismaClient(); + +if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts new file mode 100644 index 00000000..3542b7b8 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts @@ -0,0 +1,18 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import { createPool, type Pool } from "mysql2/promise"; + +import { env } from "~/env"; +import * as schema from "./schema"; + +/** + * Cache the database connection in development. This avoids creating a new connection on every HMR + * update. + */ +const globalForDb = globalThis as unknown as { + conn: Pool | undefined; +}; + +const conn = globalForDb.conn ?? createPool({ uri: env.DATABASE_URL }); +if (env.NODE_ENV !== "production") globalForDb.conn = conn; + +export const db = drizzle(conn, { schema, mode: "default" }); diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts new file mode 100644 index 00000000..4613a4c1 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts @@ -0,0 +1,7 @@ +import { Client } from "@planetscale/database"; +import { drizzle } from "drizzle-orm/planetscale-serverless"; + +import { env } from "~/env"; +import * as schema from "./schema"; + +export const db = drizzle(new Client({ url: env.DATABASE_URL }), { schema }); diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts new file mode 100644 index 00000000..1287189a --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts @@ -0,0 +1,18 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; + +import { env } from "~/env"; +import * as schema from "./schema"; + +/** + * Cache the database connection in development. This avoids creating a new connection on every HMR + * update. + */ +const globalForDb = globalThis as unknown as { + conn: postgres.Sql | undefined; +}; + +const conn = globalForDb.conn ?? postgres(env.DATABASE_URL); +if (env.NODE_ENV !== "production") globalForDb.conn = conn; + +export const db = drizzle(conn, { schema }); diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts new file mode 100644 index 00000000..ef1df14a --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts @@ -0,0 +1,19 @@ +import { createClient, type Client } from "@libsql/client"; +import { drizzle } from "drizzle-orm/libsql"; + +import { env } from "~/env"; +import * as schema from "./schema"; + +/** + * Cache the database connection in development. This avoids creating a new connection on every HMR + * update. + */ +const globalForDb = globalThis as unknown as { + client: Client | undefined; +}; + +export const client = + globalForDb.client ?? createClient({ url: env.DATABASE_URL }); +if (env.NODE_ENV !== "production") globalForDb.client = client; + +export const db = drizzle(client, { schema }); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts new file mode 100644 index 00000000..bfb08079 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts @@ -0,0 +1,34 @@ +// Example model schema from the Drizzle docs +// https://orm.drizzle.team/docs/sql-schema-declaration + +import { sql } from "drizzle-orm"; +import { + bigint, + index, + mysqlTableCreator, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = mysqlTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + name: varchar("name", { length: 256 }), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at").onUpdateNow(), + }, + (example) => ({ + nameIndex: index("name_idx").on(example.name), + }) +); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts new file mode 100644 index 00000000..bfb08079 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts @@ -0,0 +1,34 @@ +// Example model schema from the Drizzle docs +// https://orm.drizzle.team/docs/sql-schema-declaration + +import { sql } from "drizzle-orm"; +import { + bigint, + index, + mysqlTableCreator, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = mysqlTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + name: varchar("name", { length: 256 }), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at").onUpdateNow(), + }, + (example) => ({ + nameIndex: index("name_idx").on(example.name), + }) +); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts new file mode 100644 index 00000000..8e6f2f99 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts @@ -0,0 +1,36 @@ +// Example model schema from the Drizzle docs +// https://orm.drizzle.team/docs/sql-schema-declaration + +import { sql } from "drizzle-orm"; +import { + index, + pgTableCreator, + serial, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = pgTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: serial("id").primaryKey(), + name: varchar("name", { length: 256 }), + createdAt: timestamp("created_at", { withTimezone: true }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( + () => new Date() + ), + }, + (example) => ({ + nameIndex: index("name_idx").on(example.name), + }) +); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts new file mode 100644 index 00000000..cc74c86a --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts @@ -0,0 +1,30 @@ +// Example model schema from the Drizzle docs +// https://orm.drizzle.team/docs/sql-schema-declaration + +import { sql } from "drizzle-orm"; +import { index, int, sqliteTableCreator, text } from "drizzle-orm/sqlite-core"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = sqliteTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + name: text("name", { length: 256 }), + createdAt: int("created_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + updatedAt: int("updated_at", { mode: "timestamp" }).$onUpdate( + () => new Date() + ), + }, + (example) => ({ + nameIndex: index("name_idx").on(example.name), + }) +); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts new file mode 100644 index 00000000..96e9a85c --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts @@ -0,0 +1,123 @@ +import { relations, sql } from "drizzle-orm"; +import { + bigint, + index, + int, + mysqlTableCreator, + primaryKey, + text, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; +import { type AdapterAccount } from "next-auth/adapters"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = mysqlTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + name: varchar("name", { length: 256 }), + createdById: varchar("created_by", { length: 255 }) + .notNull() + .references(() => users.id), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at").onUpdateNow(), + }, + (example) => ({ + createdByIdIdx: index("created_by_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), + }) +); + +export const users = createTable("user", { + id: varchar("id", { length: 255 }) + .notNull() + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: varchar("name", { length: 255 }), + email: varchar("email", { length: 255 }).notNull(), + emailVerified: timestamp("email_verified", { + mode: "date", + fsp: 3, + }).default(sql`CURRENT_TIMESTAMP(3)`), + image: varchar("image", { length: 255 }), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), + sessions: many(sessions), +})); + +export const accounts = createTable( + "account", + { + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id), + type: varchar("type", { length: 255 }) + .$type() + .notNull(), + provider: varchar("provider", { length: 255 }).notNull(), + providerAccountId: varchar("provider_account_id", { + length: 255, + }).notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: int("expires_at"), + token_type: varchar("token_type", { length: 255 }), + scope: varchar("scope", { length: 255 }), + id_token: text("id_token"), + session_state: varchar("session_state", { length: 255 }), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("account_user_id_idx").on(account.userId), + }) +); + +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { fields: [accounts.userId], references: [users.id] }), +})); + +export const sessions = createTable( + "session", + { + sessionToken: varchar("session_token", { length: 255 }) + .notNull() + .primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (session) => ({ + userIdIdx: index("session_user_id_idx").on(session.userId), + }) +); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); + +export const verificationTokens = createTable( + "verification_token", + { + identifier: varchar("identifier", { length: 255 }).notNull(), + token: varchar("token", { length: 255 }).notNull(), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }) +); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts new file mode 100644 index 00000000..a0b1d72f --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts @@ -0,0 +1,117 @@ +import { relations, sql } from "drizzle-orm"; +import { + bigint, + index, + int, + mysqlTableCreator, + primaryKey, + text, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; +import { type AdapterAccount } from "next-auth/adapters"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = mysqlTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + name: varchar("name", { length: 256 }), + createdById: varchar("created_by", { length: 255 }).notNull(), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at").onUpdateNow(), + }, + (example) => ({ + createdByIdIdx: index("created_by_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), + }) +); + +export const users = createTable("user", { + id: varchar("id", { length: 255 }) + .notNull() + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: varchar("name", { length: 255 }), + email: varchar("email", { length: 255 }).notNull(), + emailVerified: timestamp("email_verified", { + mode: "date", + fsp: 3, + }).default(sql`CURRENT_TIMESTAMP(3)`), + image: varchar("image", { length: 255 }), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), + sessions: many(sessions), +})); + +export const accounts = createTable( + "account", + { + userId: varchar("user_id", { length: 255 }).notNull(), + type: varchar("type", { length: 255 }) + .$type() + .notNull(), + provider: varchar("provider", { length: 255 }).notNull(), + providerAccountId: varchar("provider_account_id", { + length: 255, + }).notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: int("expires_at"), + token_type: varchar("token_type", { length: 255 }), + scope: varchar("scope", { length: 255 }), + id_token: text("id_token"), + session_state: varchar("session_state", { length: 255 }), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("accounts_user_id_idx").on(account.userId), + }) +); + +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { fields: [accounts.userId], references: [users.id] }), +})); + +export const sessions = createTable( + "session", + { + sessionToken: varchar("session_token", { length: 255 }) + .notNull() + .primaryKey(), + userId: varchar("user_id", { length: 255 }).notNull(), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (session) => ({ + userIdIdx: index("session_user_id_idx").on(session.userId), + }) +); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); + +export const verificationTokens = createTable( + "verification_token", + { + identifier: varchar("identifier", { length: 255 }).notNull(), + token: varchar("token", { length: 255 }).notNull(), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }) +); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts new file mode 100644 index 00000000..5ce3f9c2 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts @@ -0,0 +1,130 @@ +import { relations, sql } from "drizzle-orm"; +import { + index, + integer, + pgTableCreator, + primaryKey, + serial, + text, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; +import { type AdapterAccount } from "next-auth/adapters"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = pgTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: serial("id").primaryKey(), + name: varchar("name", { length: 256 }), + createdById: varchar("created_by", { length: 255 }) + .notNull() + .references(() => users.id), + createdAt: timestamp("created_at", { withTimezone: true }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( + () => new Date() + ), + }, + (example) => ({ + createdByIdIdx: index("created_by_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), + }) +); + +export const users = createTable("user", { + id: varchar("id", { length: 255 }) + .notNull() + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: varchar("name", { length: 255 }), + email: varchar("email", { length: 255 }).notNull(), + emailVerified: timestamp("email_verified", { + mode: "date", + withTimezone: true, + }).default(sql`CURRENT_TIMESTAMP`), + image: varchar("image", { length: 255 }), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), +})); + +export const accounts = createTable( + "account", + { + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id), + type: varchar("type", { length: 255 }) + .$type() + .notNull(), + provider: varchar("provider", { length: 255 }).notNull(), + providerAccountId: varchar("provider_account_id", { + length: 255, + }).notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: integer("expires_at"), + token_type: varchar("token_type", { length: 255 }), + scope: varchar("scope", { length: 255 }), + id_token: text("id_token"), + session_state: varchar("session_state", { length: 255 }), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("account_user_id_idx").on(account.userId), + }) +); + +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { fields: [accounts.userId], references: [users.id] }), +})); + +export const sessions = createTable( + "session", + { + sessionToken: varchar("session_token", { length: 255 }) + .notNull() + .primaryKey(), + userId: varchar("user_id", { length: 255 }) + .notNull() + .references(() => users.id), + expires: timestamp("expires", { + mode: "date", + withTimezone: true, + }).notNull(), + }, + (session) => ({ + userIdIdx: index("session_user_id_idx").on(session.userId), + }) +); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); + +export const verificationTokens = createTable( + "verification_token", + { + identifier: varchar("identifier", { length: 255 }).notNull(), + token: varchar("token", { length: 255 }).notNull(), + expires: timestamp("expires", { + mode: "date", + withTimezone: true, + }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }) +); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts new file mode 100644 index 00000000..12ee2901 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts @@ -0,0 +1,116 @@ +import { relations, sql } from "drizzle-orm"; +import { + index, + int, + primaryKey, + sqliteTableCreator, + text, +} from "drizzle-orm/sqlite-core"; +import { type AdapterAccount } from "next-auth/adapters"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = sqliteTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + name: text("name", { length: 256 }), + createdById: text("created_by", { length: 255 }) + .notNull() + .references(() => users.id), + createdAt: int("created_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + updatedAt: int("updatedAt", { mode: "timestamp" }).$onUpdate( + () => new Date() + ), + }, + (example) => ({ + createdByIdIdx: index("created_by_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), + }) +); + +export const users = createTable("user", { + id: text("id", { length: 255 }) + .notNull() + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: text("name", { length: 255 }), + email: text("email", { length: 255 }).notNull(), + emailVerified: int("email_verified", { + mode: "timestamp", + }).default(sql`(unixepoch())`), + image: text("image", { length: 255 }), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), +})); + +export const accounts = createTable( + "account", + { + userId: text("user_id", { length: 255 }) + .notNull() + .references(() => users.id), + type: text("type", { length: 255 }) + .$type() + .notNull(), + provider: text("provider", { length: 255 }).notNull(), + providerAccountId: text("provider_account_id", { length: 255 }).notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: int("expires_at"), + token_type: text("token_type", { length: 255 }), + scope: text("scope", { length: 255 }), + id_token: text("id_token"), + session_state: text("session_state", { length: 255 }), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("account_user_id_idx").on(account.userId), + }) +); + +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { fields: [accounts.userId], references: [users.id] }), +})); + +export const sessions = createTable( + "session", + { + sessionToken: text("session_token", { length: 255 }).notNull().primaryKey(), + userId: text("userId", { length: 255 }) + .notNull() + .references(() => users.id), + expires: int("expires", { mode: "timestamp" }).notNull(), + }, + (session) => ({ + userIdIdx: index("session_userId_idx").on(session.userId), + }) +); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); + +export const verificationTokens = createTable( + "verification_token", + { + identifier: text("identifier", { length: 255 }).notNull(), + token: text("token", { length: 255 }).notNull(), + expires: int("expires", { mode: "timestamp" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }) +); diff --git a/packages/cli-old/template/extras/src/server/next-auth/base.ts b/packages/cli-old/template/extras/src/server/next-auth/base.ts new file mode 100644 index 00000000..ac29d61f --- /dev/null +++ b/packages/cli-old/template/extras/src/server/next-auth/base.ts @@ -0,0 +1,111 @@ + + + + +import { env } from "@/config/env"; +import { OttoAdapter } from "@proofkit/fmdapi"; +import NextAuth, { type DefaultSession } from "next-auth"; +import { FilemakerAdapter } from "next-auth-adapter-filemaker"; +import { type Provider } from "next-auth/providers"; +import Credentials from "next-auth/providers/credentials"; +import { z } from "zod/v4"; + +import { verifyPassword } from "./password"; + +export const fmAdapter = FilemakerAdapter({ + adapter: new OttoAdapter({ + auth: { apiKey: env.OTTO_API_KEY }, + db: env.FM_DATABASE, + server: env.FM_SERVER, + }), +}); + +/** + * Module augmentation for `next-auth` types. Alldows us to add custom properties to the `session` + * object and keep type safety. + * + * @see https://next-auth.js.org/getting-started/typescript#module-augmentation + */ +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + // ...other properties + // role: UserRole; + } & DefaultSession["user"]; + } + + // interface User { + // // ...other properties + // // role: UserRole; + // } +} + +const signInSchema = z.object({ + email: z.string().email(), + password: z.string(), +}); + +const providers: Provider[] = [ + Credentials({ + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + authorize: async (credentials) => { + const parsed = signInSchema.safeParse(credentials); + if (!parsed.success) { + return null; + } + + const { email, password } = parsed.data; + + try { + // logic to verify if the user exists with the password hash + const userResponse = + await fmAdapter.typedClients.userWithPasswordHash.findOne({ + query: { email: `==${email.replace("@", "\\@")}` }, + }); + const { passwordHash, ...userData } = userResponse.data.fieldData; + const isValid = await verifyPassword(password, passwordHash); + if (!isValid) return null; + + return userData; + } catch (error) { + console.log("error", error); + throw new Error("User not found."); + } + }, + }), +]; + +export const providerMap = providers + .map((provider) => { + if (typeof provider === "function") { + const providerData = provider(); + return { id: providerData.id, name: providerData.name }; + } else { + return { id: provider.id, name: provider.name }; + } + }) + .filter((provider) => provider.id !== "credentials"); + +export const { auth, handlers, signIn, signOut } = NextAuth({ + pages: { + signIn: "/auth/signin", + newUser: "/auth/signup", + error: "/auth/signin", + }, + callbacks: { + session: ({ session, token }) => ({ + ...session, + user: { + ...session.user, + id: token.sub, + }, + }), + }, + adapter: fmAdapter.Adapter, + session: { strategy: "jwt" }, + providers, +}); diff --git a/packages/cli-old/template/extras/src/server/next-auth/password.ts b/packages/cli-old/template/extras/src/server/next-auth/password.ts new file mode 100644 index 00000000..a82f34c6 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/next-auth/password.ts @@ -0,0 +1,13 @@ +export async function saltAndHashPassword(password: string): Promise { + const bcrypt = await import("bcrypt"); + const saltRounds = 12; + return bcrypt.hash(password, saltRounds); +} + +export async function verifyPassword( + plainTextPassword: string, + hashedPassword: string +): Promise { + const bcrypt = await import("bcrypt"); + return bcrypt.compare(plainTextPassword, hashedPassword); +} diff --git a/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts b/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts new file mode 100644 index 00000000..6e9281d1 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts @@ -0,0 +1,83 @@ +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import { + getServerSession, + type DefaultSession, + type NextAuthOptions, +} from "next-auth"; +import { type Adapter } from "next-auth/adapters"; +import DiscordProvider from "next-auth/providers/discord"; + +import { env } from "~/env"; +import { db } from "~/server/db"; +import { + accounts, + sessions, + users, + verificationTokens, +} from "~/server/db/schema"; + +/** + * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` + * object and keep type safety. + * + * @see https://next-auth.js.org/getting-started/typescript#module-augmentation + */ +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + // ...other properties + // role: UserRole; + } & DefaultSession["user"]; + } + + // interface User { + // // ...other properties + // // role: UserRole; + // } +} + +/** + * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. + * + * @see https://next-auth.js.org/configuration/options + */ +export const authOptions: NextAuthOptions = { + callbacks: { + session: ({ session, user }) => ({ + ...session, + user: { + ...session.user, + id: user.id, + }, + }), + }, + adapter: DrizzleAdapter(db, { + usersTable: users, + accountsTable: accounts, + sessionsTable: sessions, + verificationTokensTable: verificationTokens, + }) as Adapter, + providers: [ + DiscordProvider({ + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + }), + /** + * ...add more providers here. + * + * Most other providers require a bit more work than the Discord provider. For example, the + * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account + * model. Refer to the NextAuth.js docs for the provider you want to use. Example: + * + * @see https://next-auth.js.org/providers/github + */ + ], +}; + +/** + * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. + * + * @see https://next-auth.js.org/configuration/nextjs + */ +export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts b/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts new file mode 100644 index 00000000..117984c9 --- /dev/null +++ b/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts @@ -0,0 +1,72 @@ +import { PrismaAdapter } from "@auth/prisma-adapter"; +import { + getServerSession, + type DefaultSession, + type NextAuthOptions, +} from "next-auth"; +import { type Adapter } from "next-auth/adapters"; +import DiscordProvider from "next-auth/providers/discord"; + +import { env } from "~/env"; +import { db } from "~/server/db"; + +/** + * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` + * object and keep type safety. + * + * @see https://next-auth.js.org/getting-started/typescript#module-augmentation + */ +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + // ...other properties + // role: UserRole; + } & DefaultSession["user"]; + } + + // interface User { + // // ...other properties + // // role: UserRole; + // } +} + +/** + * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. + * + * @see https://next-auth.js.org/configuration/options + */ +export const authOptions: NextAuthOptions = { + callbacks: { + session: ({ session, user }) => ({ + ...session, + user: { + ...session.user, + id: user.id, + }, + }), + }, + adapter: PrismaAdapter(db) as Adapter, + providers: [ + DiscordProvider({ + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + }), + /** + * ...add more providers here. + * + * Most other providers require a bit more work than the Discord provider. For example, the + * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account + * model. Refer to the NextAuth.js docs for the provider you want to use. Example: + * + * @see https://next-auth.js.org/providers/github + */ + ], +}; + +/** + * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. + * + * @see https://next-auth.js.org/configuration/nextjs + */ +export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/packages/cli-old/template/extras/src/trpc/query-client.ts b/packages/cli-old/template/extras/src/trpc/query-client.ts new file mode 100644 index 00000000..bda64397 --- /dev/null +++ b/packages/cli-old/template/extras/src/trpc/query-client.ts @@ -0,0 +1,25 @@ +import { + defaultShouldDehydrateQuery, + QueryClient, +} from "@tanstack/react-query"; +import SuperJSON from "superjson"; + +export const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 30 * 1000, + }, + dehydrate: { + serializeData: SuperJSON.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", + }, + hydrate: { + deserializeData: SuperJSON.deserialize, + }, + }, + }); diff --git a/packages/cli-old/template/extras/src/trpc/react.tsx b/packages/cli-old/template/extras/src/trpc/react.tsx new file mode 100644 index 00000000..8c0521a7 --- /dev/null +++ b/packages/cli-old/template/extras/src/trpc/react.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { QueryClientProvider, type QueryClient } from "@tanstack/react-query"; +import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; +import { createTRPCReact } from "@trpc/react-query"; +import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; +import { useState } from "react"; +import SuperJSON from "superjson"; + +import { type AppRouter } from "~/server/api/root"; +import { createQueryClient } from "./query-client"; + +let clientQueryClientSingleton: QueryClient | undefined = undefined; +const getQueryClient = () => { + if (typeof window === "undefined") { + // Server: always make a new query client + return createQueryClient(); + } + // Browser: use singleton pattern to keep the same query client + return (clientQueryClientSingleton ??= createQueryClient()); +}; + +export const api = createTRPCReact(); + +/** + * Inference helper for inputs. + * + * @example type HelloInput = RouterInputs['example']['hello'] + */ +export type RouterInputs = inferRouterInputs; + +/** + * Inference helper for outputs. + * + * @example type HelloOutput = RouterOutputs['example']['hello'] + */ +export type RouterOutputs = inferRouterOutputs; + +export function TRPCReactProvider(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + + const [trpcClient] = useState(() => + api.createClient({ + links: [ + loggerLink({ + enabled: (op) => + process.env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + unstable_httpBatchStreamLink({ + transformer: SuperJSON, + url: getBaseUrl() + "/api/trpc", + headers: () => { + const headers = new Headers(); + headers.set("x-trpc-source", "nextjs-react"); + return headers; + }, + }), + ], + }) + ); + + return ( + + + {props.children} + + + ); +} + +function getBaseUrl() { + if (typeof window !== "undefined") return window.location.origin; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return `http://localhost:${process.env.PORT ?? 3000}`; +} diff --git a/packages/cli-old/template/extras/src/trpc/server.ts b/packages/cli-old/template/extras/src/trpc/server.ts new file mode 100644 index 00000000..59300a63 --- /dev/null +++ b/packages/cli-old/template/extras/src/trpc/server.ts @@ -0,0 +1,30 @@ +import "server-only"; + +import { createHydrationHelpers } from "@trpc/react-query/rsc"; +import { headers } from "next/headers"; +import { cache } from "react"; + +import { createCaller, type AppRouter } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/trpc"; +import { createQueryClient } from "./query-client"; + +/** + * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when + * handling a tRPC call from a React Server Component. + */ +const createContext = cache(() => { + const heads = new Headers(headers()); + heads.set("x-trpc-source", "rsc"); + + return createTRPCContext({ + headers: heads, + }); +}); + +const getQueryClient = cache(createQueryClient); +const caller = createCaller(createContext); + +export const { trpc: api, HydrateClient } = createHydrationHelpers( + caller, + getQueryClient +); diff --git a/packages/cli-old/template/extras/src/utils/api.ts b/packages/cli-old/template/extras/src/utils/api.ts new file mode 100644 index 00000000..0f03d307 --- /dev/null +++ b/packages/cli-old/template/extras/src/utils/api.ts @@ -0,0 +1,68 @@ +/** + * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which + * contains the Next.js App-wrapper, as well as your type-safe React Query hooks. + * + * We also create a few inference helpers for input and output types. + */ +import { httpBatchLink, loggerLink } from "@trpc/client"; +import { createTRPCNext } from "@trpc/next"; +import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; +import superjson from "superjson"; + +import { type AppRouter } from "~/server/api/root"; + +const getBaseUrl = () => { + if (typeof window !== "undefined") return ""; // browser should use relative url + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url + return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost +}; + +/** A set of type-safe react-query hooks for your tRPC API. */ +export const api = createTRPCNext({ + config() { + return { + /** + * Links used to determine request flow from client to server. + * + * @see https://trpc.io/docs/links + */ + links: [ + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === "development" || + (opts.direction === "down" && opts.result instanceof Error), + }), + httpBatchLink({ + /** + * Transformer used for data de-serialization from the server. + * + * @see https://trpc.io/docs/data-transformers + */ + transformer: superjson, + url: `${getBaseUrl()}/api/trpc`, + }), + ], + }; + }, + /** + * Whether tRPC should await queries when server rendering pages. + * + * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false + */ + ssr: false, + transformer: superjson, +}); + +/** + * Inference helper for inputs. + * + * @example type HelloInput = RouterInputs['example']['hello'] + */ +export type RouterInputs = inferRouterInputs; + +/** + * Inference helper for outputs. + * + * @example type HelloOutput = RouterOutputs['example']['hello'] + */ +export type RouterOutputs = inferRouterOutputs; diff --git a/packages/cli-old/template/extras/start-database/mysql.sh b/packages/cli-old/template/extras/start-database/mysql.sh new file mode 100755 index 00000000..268df5cc --- /dev/null +++ b/packages/cli-old/template/extras/start-database/mysql.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Use this script to start a docker container for a local development database + +# TO RUN ON WINDOWS: +# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install +# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/ +# 3. Open WSL - `wsl` +# 4. Run this script - `./start-database.sh` + +# On Linux and macOS you can run this script directly - `./start-database.sh` + +DB_CONTAINER_NAME="project1-mysql" + +if ! [ -x "$(command -v docker)" ]; then + echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" + exit 1 +fi + +if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then + echo "Database container '$DB_CONTAINER_NAME' already running" + exit 0 +fi + +if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then + docker start "$DB_CONTAINER_NAME" + echo "Existing database container '$DB_CONTAINER_NAME' started" + exit 0 +fi + +# import env variables from .env +set -a +source .env + +DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}') +DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}') + +if [ "$DB_PASSWORD" == "password" ]; then + echo "You are using the default database password" + read -p "Should we generate a random password for you? [y/N]: " -r REPLY + if ! [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Please change the default password in the .env file and try again" + exit 1 + fi + # Generate a random URL-safe password + DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_') + sed -i -e "s#:password@#:$DB_PASSWORD@#" .env +fi + +docker run -d \ + --name $DB_CONTAINER_NAME \ + -e MYSQL_ROOT_PASSWORD="$DB_PASSWORD" \ + -e MYSQL_DATABASE=project1 \ + -p "$DB_PORT":3306 \ + docker.io/mysql && echo "Database container '$DB_CONTAINER_NAME' was successfully created" diff --git a/packages/cli-old/template/extras/start-database/postgres.sh b/packages/cli-old/template/extras/start-database/postgres.sh new file mode 100755 index 00000000..11fb2042 --- /dev/null +++ b/packages/cli-old/template/extras/start-database/postgres.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Use this script to start a docker container for a local development database + +# TO RUN ON WINDOWS: +# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install +# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/ +# 3. Open WSL - `wsl` +# 4. Run this script - `./start-database.sh` + +# On Linux and macOS you can run this script directly - `./start-database.sh` + +DB_CONTAINER_NAME="project1-postgres" + +if ! [ -x "$(command -v docker)" ]; then + echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" + exit 1 +fi + +if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then + echo "Database container '$DB_CONTAINER_NAME' already running" + exit 0 +fi + +if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then + docker start "$DB_CONTAINER_NAME" + echo "Existing database container '$DB_CONTAINER_NAME' started" + exit 0 +fi + +# import env variables from .env +set -a +source .env + +DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}') +DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}') + +if [ "$DB_PASSWORD" = "password" ]; then + echo "You are using the default database password" + read -p "Should we generate a random password for you? [y/N]: " -r REPLY + if ! [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Please change the default password in the .env file and try again" + exit 1 + fi + # Generate a random URL-safe password + DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_') + sed -i -e "s#:password@#:$DB_PASSWORD@#" .env +fi + +docker run -d \ + --name $DB_CONTAINER_NAME \ + -e POSTGRES_USER="postgres" \ + -e POSTGRES_PASSWORD="$DB_PASSWORD" \ + -e POSTGRES_DB=project1 \ + -p "$DB_PORT":5432 \ + docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created" diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/de.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/de.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/de.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/en.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/en.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/en.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/es.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/es.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/es.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/fr.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/fr.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/fr.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/icon.png b/packages/cli-old/template/fm-addon/ProofKitAuth/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..037908f6ad5023cd041e703f8aa755f100b08852 GIT binary patch literal 38399 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 Zd#48CZ1u|OmK7l2(Nxn_C7^7={sWnjNg)6L literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/icon@2x.png b/packages/cli-old/template/fm-addon/ProofKitAuth/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..037908f6ad5023cd041e703f8aa755f100b08852 GIT binary patch literal 38399 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 Zd#48CZ1u|OmK7l2(Nxn_C7^7={sWnjNg)6L literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info.json new file mode 100644 index 00000000..926bf18f --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info.json @@ -0,0 +1,8 @@ +{ + "GUID": "77BDB4D6-5CF8-4740-AB5F-19837A831C21", + "Clients": ["ProofKit"], + "Attribution": "Proof+Geist", + "URL": "https://proofkit.dev/auth/fm-addon", + "Icon_Color": "#7F7F7F", + "Version": "1.0" +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_de.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_de.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_de.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_en.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_en.json new file mode 100644 index 00000000..cb5afe39 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_en.json @@ -0,0 +1,8 @@ +{ + "Title": "FileMaker Add-on Auth", + "Description": "FileMaker Add-on Auth - User authentication with FileMaker web apps", + "Version": "1.0", + "Category": "Web", + "Type": "fmaddon", + "RequiredFMPVersion": "20.0.0" +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_es.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_es.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_es.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_fr.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_fr.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_fr.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_it.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_it.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_it.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_ja.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_ja.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_ja.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_ko.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_ko.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_ko.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_nl.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_nl.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_nl.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_pt.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_pt.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_pt.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_sv.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_sv.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_sv.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/info_zh.json b/packages/cli-old/template/fm-addon/ProofKitAuth/info_zh.json new file mode 100644 index 00000000..e12bd9b8 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/info_zh.json @@ -0,0 +1,11 @@ +{ + "Title": "FileMaker Add-on Auth", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "Web", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/it.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/it.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/it.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/ja.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/ja.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/ja.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/ko.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/ko.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/ko.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/nl.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/nl.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/nl.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/preview.png b/packages/cli-old/template/fm-addon/ProofKitAuth/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..037908f6ad5023cd041e703f8aa755f100b08852 GIT binary patch literal 38399 zcmbrk1ymi)wk}Et?(P~AJh(dqcMI;a(4Y%beC#@r@Uf&vJ9t~>z1H&d`7 znd>)e8%G{jeyV?Pd4S)4Z!=N>LJlS%9%WJSe~SPuekyY?*p7#h(Z$7u!G)E<*1?RC znVXxNk%@(oh2=9K@!8SM25jj1*~XCqKtLe-2Se1<(b&Pl4s2m-L-rTb(8$&a%uhvy zK=yBFVKCUi!stKfHjWI2|J-r<+l2uzn$gh2k&&5!>91O3WPJaU=Mgq>{M-E3AS$Nz z|1|#9vbOkVBs)U~M^jZdJ5zotWm896CkJEGe-Qq5`ZvfI2UA0^sfhp!6ARmCW@g~v zWBgwp{T<`KI*7S~rA!3=OX^?k|0yGG1c%Wd$nr7%b)B`vzbyWz@o$s=)8k+M_?O^+^ZEb(IG7m!CkA#-4%Yv~ z*Tk67)Y|l$sSWsV1epIF0TW{$um#xK^uPQsYz_XO{s)M~V{K?-#!uz?*~AoN=wuD1 z5^yB@Oa{2_|6$6%d-E~=H{kyRL+t(=SO2o#`6yeZ5>o?ZLI~q7}_`+ zl2I$0+L)L+m^#q#G5)U-|F2s8OHm+Qfy8C}PkIB5|4a}7AU~A@kp7<%^XwrY;R3H28nvi2G??=j;$t-Y{@^iV{fK_U zh<}cIOMrNn2qFLHqr6f7cjP{2lf_!%MJ$TYg-BU(o;Fo}=(meKsxxM;?e~JTBP|0bE5qoiX-B;K=)+Vuf&l zE2S?~8=43l^Sh4|L1-eO@ML}rfRHQ%JQxr*f|Sff1g@Aukh%a!D#ZV1*Hl@m-K@i! z>^phS&y!&Z=HFV2SGy*)UsBrbk4WF&G$3M$5HSFll*7l7Dvyry4tqW~5k+R*9L6c$ zn{PU(M=xj1YY%HtuZOmrn`46SvCR7ISVh zNTHWk&l=WV*Q#Cv-YT-LU-2h+i+O_3Bt#)~Loar5=+6{%ujOAGYm&3yhNM9Zf+G>? z0i$!I?CX!O#BY=9#~)wT-{(`5tH9a!0jA?Az??^!IqrFH;SMo+UVxqJE;V0k+g~3~ zn=@>m|G1y6y+L_i*+~OtPyuHA8lTU(kbj-;R`k9|LR=dyi7d#ex zCiOid+RgbK$fX6C5E)aW1L%^X|JmzQCdIKNCHfL`b-$+CtNG!Ukn^$PjsJYL;8{l` zw3pnEp>O|bv+=q0?K$YV-&SWdi{Ao&x6V^JXcP1%yaiiz*X7vha@PI6%ZENk1J0i8 zFMKtlz*g2{lzMndw`V`E;}r9WYIvyAPmj*_X8og;`$_qQ`~QB>NQnC6yS@Zqr-omDba*JGFqQw$?{ z#9Vec^=Ne-H^pp={zU!`eKwP7*lc>Eb}jLEg3XvbeRE!4zb7V>ZD$}aP6DWSUAh9QTI&Uf;`F%&aP_Oegj_o%d2tlbXMnEz&&+E< z(a4CtPm$eD5fBiCZ<9PWr?7+_vk&K;YEv(QYX@tu?LTam*x{K!1M%h}X@oAhji9Q( z^0`U+nUpeal0r%QJEsBX^jz((7j@1*tJ|Z#`?1+Cs=*4wXi6#<(Y_%$c_mHIgR=b^ zSD!}5>p>L6k>D&mU_nuNC4YB&-<>ki56&MrMH)B8#$T@icpx5a@ z*7<-@cQosvA7H0my3^2ro>1EVNg+X1iilMF6sBee(}r!-!umFv?)%{ zZO(3CNw3x3&eh&}-cULmkHz4a^ZuGmG@f%O|NQv%Y-GNAXPjTXE&{--#ME*R5osD2 zW;IRjSfP6DdRj%PIc1Z|XE7}ZPWu_!nRR`i>+-gCv(|a0f42HU>PJfKfi5D82ari1 zBZbkG&gTVSYfg7OuVb}=+kl?0$h4{xQS%@jE4KVj&Z+b@Zx#mtNM5bXZWi4#q%f!c z4hZ3`4WQu-kn-!+RGosdMQHz;jC$*RYjn@mUYAlL=Ji_tRAr8(+t3Cbm+?eTZ}z7_ z?jgWtV7DyVT>i4br6^bAi1Dc+g*kcw@Q&84I(f}G}>*3!& z;3D9@)u$k&c)yA`n{rfvnB*E|4CWqw>V(Pk_iC2B<<4&3tm0&+Q%LM6zK#U6oCqFv zEq7k+78pD=pTQD#zn@W|6Axwn0OaSma}fwvzjZ(Bx#({Od2_V0WvG#Vp8d$w{O~O6 zN4L|9x+>Z`>KrhCsJr4C{rxM?_4q{t35<5f;>&r|sNLP!>T%7h${%OTemG_rASm}H zYqKk9`h)4GHTq*&txd;cVwLpa7ECAT6z!}5uxUi-%3W;Rb%0V*r5^?0jOl;A^Z@({H2Ip1|}xM~LSU>$Z& zg-$XQodPiZM?d0PShpvK$L6^1aF!QnQqFAt&KP<21Cz6Bb@Gm7naFNxz_XNh{JC>M ze^e)g`h0027dC1w$(&n-$I(ql&n>4d5FQ*midp|aymuJ;MoV4~4yV`pUK}pBSGXNX z6&CWJ-yeTM7O5Y*YUEQN`lRF7U}JEF+3zt5b9QoLKa2F0uG19GceSNo*T?H-^)%~D z7a%0zk2{smlA%~qxe*qs6^li-Bki-@m(m+Uhk+SvofHrF^ir8DtT}UqgHE<_Tb3d4qYoNW7aT( zzO-XA50X~H+?H;wb_bvOAIpRLr%uu!!YUlwvl}>nKJ1G0Ly!pGadh{V;8N$AZATl) z+*?Yc-Cl-Js-=<$5ji}Vxh$JIL-#ewt>kie{&FveH!baMDq9Uzwa?~zNXo(0rfjrX z_`wODp!8kaH@#IGOmXgT@u-HjeSRP}D_+IVL3h3L^FOoJm*9@pMaCK;;6M5?P)g;Q z?D)KzfHvkCU8%CWf!xe+DYuvx)rUU*2bbx^4iO3d#(P)5g=ozWikJ$&k%5K!k?x3)_R|^L#%G+Mc%9DvJp>? zgv(O2+>GmkL!2P%9LA9(t_zyx9Jw~9?s4rK9H({43d=%!ztIFw?C%8DGlKh7UG4)u z*AhpDFL!>;zFa3ufn3yp8>(@}J*d1bddgmx21kA(mas$=v7M5MiKUr&CmE?IlR;qn@`T_e_<=T%E z{ak~Pi~>2`=eT>ThdCYlSse|VV}|%@OHS`y;*zwjGRPMCOa*UD_1% z7etHq@2D;h30P39?0DC9HQ~&yAANf9xpBU`6@*lqu9gWc0SbU$J@r5)2=bn(dLQJw z93BpicvhQ7ja}c!&wOk+)TscqDl;h6POq-=Gql-{c#PtZvOT|0vKKJ-LIxzjS#6-8 z22|~W8t$AK58iHc8-g zwp_i>&2j!F;+YA6V^f|VhmEW-ZjT=O*e&wv^6q81+7D()-Bm7PYDuLm>>DlXW>@UE z6>Aucxc5ZT7j1yr`%zt%@TB61#4agn{Z6;E>{CKEn%=z6)_8A9A5d+cVqVc)2^B5o zDFgY!MbnA?3;~6aeq(LdYoyENjI^`2TTfZk3g@o;jL_m|`JHbJiS;V^=`%bOX zr&pyFwec)T=;_ful_Z?4do}Crvp)A@HZza6F9ut8zDK_D?HtTm;wP;%l=?6&x1O&* zYg&u1ZYo+?Q$ShaoCtgUIar6Ot1W)twfOXh?Td8!>cx%UuQlxVF>`8vUMqF6cScqSG>M4D?*=E+Emd zaG&_j7C8w{jS&k0L2Z*k!m1l>=GwKh3-``m=kdM**?y8$oVJ_NLGgy=)}xxvc1-p) zw{Dj=(s!JGPk`SkV*qqnTzOL*Z<}8+mE zD=_nN?E=iNfQQVKpc!?Vh`$pwsP475uQSRY5s3AFWkq9zK0KyCT)T zFQCc-sFd05S$W~0R8Bj#C z4UF)(|2$`^xY^Iq_!NLr7_&whvh?k&8o5jSnK#$v>*N=rv;#p2JDH=1jYKlvwY~EB zj-A|UCJozBqsx_=5GdZf-~+Z|wKw#xqPv}63~QaN_$z%%lECYRc62MwOE+N`Opoga zALJP9+DvwGw8utX2mO=Uohj>G9R{)AQ6c25c4@v4ET(MzPl@7=OYp=G;bE5~L;mavN*bJnfFC5=n ze`E)T^z|IfUIWcc1zpHJj4Z~=GIQ*QB9+@`b3VJo2bOz5S3K)gUDP8~5~z$foR;P{nWaetAxN*KSzlH$ zIv0zcDz8o>Y=*2MhD;6MWt8g4rUvxAv4?%eZ|itRit4Y9nb!#0*4(@dxujH;x4bLn zx8@HVAmw}`T#540kdxad3Ybht`D&BhZsi6C#>^4=kGbX}Dbmnt zmu?TiIA4~ppH|GBT2~K^%UIkBxHyhZazyhi`o1;w?pEEHw|9}uwdaLbpY@*{0jtnw zg8<=)Cz=#mlr+9K%3i)&Bzh0fF0lFacKgJtuLZU&NoK82DR-{Z_#f#snIa-8zL;=cfsFLN`-_A?k5vXvZGQjXl;KxcQIOT# zUG-WDhZVj+Dl=NHu7^6$bNqC7WIR2-r*(DVuk407X2Kh^E!HV_mB!dF7=*;}4NBML zVLwvUvzL1+Hv=UnuP!PN4l$}7i&UBKQEPF_{<2aTAD>cxmpA!|c?nCmKS(*+lu=?Z zEikX4Dp_Hg(;ghw5tmVF=R*%{YzrlL51_R-)&fpSmDa;=mneT}TWySK zd)PmHfuVRn-WIrzqwve_;)W?WZ8dedeaKJpSYyw+LX>Z0IZ{owlDFWnb;tT_TN%BC zR8a%jRvysAJHwJ5QA8A>7o9I#5`jJi_l4vJi1Y1!AWjo)9BXG-YVsQ!!Tv{nU=iyq z?X1H;sRnkrH>;(Mt%S{b->5nzki;*YVC3xlRS~m|F!l#ZMhs^79XZ;srRPR;SeeJh zxpt;1RjS8$l6L%e+{EZOo7zW1X5}8is<_`8HAfaRKey3)xFKL{8X>V2#5%3-snttR zOmMaN79bjT=Gox+ApqpRjhFAlb^(08x~sPWaI7lIedgIv{XzQ&PC)EBB+r2w-^0fV zRDF`voVsBu+|?zzXbXMlG2E(^kxJd_QJ&eI%%nW?9cqm=jfNsVdICA#gGqI-4+Z^3 zYr77M#xzHRzc`rZ-0eq#*Y^y^%^Huklz&KGZb$On1|FNaY59fk)8v3w78j$m33-(q zYtq03RV_zW*PTLQ&1wOo&Az%YhU>De+hNT777q6gNX0{DW1tj$T8*_m>)y6>*7}Hg zc{lH0G~>G?^kTyW20O*4A&+%JK@ABcKS-yM^``5ZX=y>4B|2$Nv@&kP!&vk&{yR}Oi>JM4M}hKF zB_-|}&O&2STx%r+0?Z%)otV@)tmDe}~#mo;+q;$_G77!RkkN6r5z<*!?_L zl5qVKwBpu)&7R)bw{AhXO1gF^=o%pGnEBZNnOpF!3{atRh^%GLZ!sY)DCK*x2QWb= z4mlf|0`oM3R;x(Qh*E963h7D=f42D9M=ztt7Wuw1o#5#rqzHV4rRrky(X;b4y%4fV zb276GoeCU{-;q8F{`N`c3kZ+a5AR%-y_U!|ctVOCRQMC_EZV{p=niTLn{BJ1d9?|Z zIx)-ce}NjF^e9eOOwi6_V&z?bfMMLbq<_=EWqaK2P+^yG_V%7DqgSi^ zIr#hOD_YAD%QsyJYaM5{&uFf{$@bf6QrwS=KTR=6>UgJA)6pd7(xamW(s`Bxdwj3r>g%p4t@k!~ zE7cuM5%oREH6%~o&p8q;H!dETu_5_csWd}Aln3{j6;Cvq#y7h&i5#<^XYo1rXMwnv zxIS0o<1ZfFx;qCz^M-Tj zf_88IHk&F6O{v2Yo=#mu)}ZI=(JY)e<(XdJP9~Eo((_!*Kk-yj*7V<9LBT6foUPoY z%dZr#bB}^q#aMsm;7ODrQ?Cj{ccr4)}+lKRl8YNVzxiMk1x*`p$qUp}i8ACAer z@^-*0qCP#Bt{F6%Dzc_o!wn#7+ z%N2nn_S-3ds~v$_me-(dqjyEYrjqEd+8p9OuTw^S>gZ}8upG{{rnej7UWo(u3@O!) zO&mHsta@l?(y+VMsTJ)G+4m}`+3V6)52wXcr8eY;s$oq}4VHJqww?iWw&d^qOHP`sS9%F(jO zUhBEMZ4;U$G0m%NvJByrWeLS~qe zlbRe-+NQ6P&sn#%Rk}XxU{OhOJjVRNfoxeVa}u^yvdUFVEYVO9YBL4u#GORK7PdC9 zg#BX1WIsrNft+S z&VD1wicXxGlher?$L$-ID>aT;G9TrhYihgnY|?W^^=2Bgj1E5SZR@T~XN%3)oo^W( z-|AC|ts_12Sngc~<;@4kv$#Nv$F-|5Ww6Y`ki}!U7%3yK<^sk%6w`PTdGQTaTqp$> zHT-!nJ;WOozCsez6hGK7Wm!SL9J%5eHP5ZX<)hSpNKZH}ULZ}k=p8B6 z=*Z1LztiZTl=_-Utc~06?k_#l%n)W9WH2R;FB)E+F-cLA@`=UR3PMM{W4?9w>)~Ni zhDXLhe={x>HqMMZ*d%eBpsT_VB0i#$>*<4S`?etAZT)%X!oaLG-0nEE?>x*Y)XD;M zg)h&I!>XB)pH*VeZt@x4=G|Gd^FxpPdx~lndS z!{U)7W#s8tiz187bV}AVB;eRX5PWql!~z0?Ee785z0u2&;Lo9M3dIbG+}KRR=VNs& z#)A3K{!hFq!s#*VWc4W!{D4 z{HJU-T@iwt2hSL9M9Ee~5O3V&WfK-h8$U>Cd!%&jf=KC6|#einZ9U3QMWYndAF_xwJS<@5HA)_H?Y>LVf_GZ6U610UfMYg{w0cq9f`woqux4p5s37_aG%T3O{9xSM~F#BA_LOR0v zzF5d4yZYJfPVpG|*gPYKI%_Grcd92J6bn}mF2=RnM|O^M%D1^vDuxbFn6bQkcR5&g z1jsQ|^C{mN{3R##wy(rH`)|Z^AX!Ja5mmEjh@u69@{e+{Ar<4>BLgr`+4R2b*FeZgifiU5h zZw@e$CMqi?6Xt0SxUkOM^V_T95xVQYqAN0M6%=|D4B57PwNEH*=a#w3r6)BE*Dj0yl$c)6+{Sb| zwRv?du2D3do_kjPU=>>w<%~+4!-K`kd!mJezrHZT6K6Vx&@Di1xDbh~(V#oM)9PrS zFGkl$wNFYIqlv}*N|9&ahDDFskHOhQ*wNZKEGvIyy|^XYCF(r#_$ML&|2Tx;b&=>u zbZ&x5xNwupu%+u62Bt1q>4j|psZGbW;h}c5SKd;!GJ!r_GgiKcVzAF(tqk((qww#$ zd4lJ-n@|<#kcuQoIz6eia0Mo4*mMU4Nt2YPkCKXhJVIPnpJRfgX2T&*!P*_I%b%hV zwxbA2j#*gXsaK;)D`f5K(5dUozzFSqP6VIRcpfv-*QLpCrzsYH}S15`D+`OD`0 zz~W}BmY?GTB)fVvjyzoL7Z#$l#37h0hprf^rsf_~IUVvu9HOc6a@ z>j!N3ySjXXeaHO(6?q=M{CM%9KG;W_l|z#41+9-}^8>%+7xsi}dPhMyO}1P6EbJbB zz%1v;cN#%w@NL?BTFEe zc0&nZdQa0%4;>1nF&BLcdv36D_^f+~c0-R=LAu{n5z+8srgb?XIs*T-!~ScjUhxW* zXk`Nl?Zi8b!7Ej0T0^@1y16s^nv>}$*-#m*D(EpSw^$_HkP)_#7>4k7`?p@+3kSI) zFu{#u4@Q0}ZYdA4*&Ux*g)NvPH^wS0*qRKTqf0a6Sdjg)19^xpSJTKTu8na@1Kk(Q z=+1%nG<8Q-)?a@V$qs528}a-p)-6h<{9BX`LDqlNs+-)u+^|VmSRp&39>e;cNwZzr zs_Bpn^=u#72wISE%*T$44mo1rC@%gmL6L>TziKqp7$s4jA{qd3_q1|$e+*6VWyc=u zm(M;xLq}0y$IL?CTWEw^rU-=tpAh=gt}C!`5{Ku3rPF`lnT**=U!O)KroHAx64Y~3 z)?1xN$96BFGHIR`EEGe5jf%Yw^Jie{@rLOn*vt7#PS%83J8At9oov>Cj+}yY3FnPvnYP1tFe_AGPut zPDz!dC`@S~`R4}hc!`h$G>TVHD~Utc3Vc^500+6vAlD8We&j}C|I#y3XTB8RA`iTS zfNTlwjbv@XM^NYehlK?#)lhOCD6TXIc1C4Xe<;4S_6@s79R_ zIYA|gap1a7frzm$m@$>EMl=yF()8>rCul$g$J7u}z>k#GJ6lh$^~4(}_wh%+YAsU2 zcU=esQm;xzM?l{bkCC!hi0;Z7Koc4uICzAjq`!j@ z9dMECLc=(qyhvqn6=H|X1jJT(|8_>3Kr)s+F{DKu_?^IMmfFlGd&|o`h*W?^dNG2O zowjj7O+u!Ke$zA_5FBH86S~mTgjJu0urX^DQ5VoJGOdL#3I4Wj?X>NRUz4&wihH~RyzJ#(Z28#_2DSh|; zj{Qv#PU8f2h5QGTmr;7Enn;h|L*&12G)tC*+`*(rf zZ!b@#_ZS7ppg^-;>tBZC8RTZXiPk9{C-KY!cYNkxUye0tD^G0cT!m1mFQ_m54Fi=i zj=301_L3ex<(^fzb(o^}b-$)Gt@*x-ql}jWlcOEW^m!4k(WR+uO60-i1+uF-TtMiF z_mA{wrYeEx=|bYCmSC}-I*lPy%^_~p%#r)djClhT4{KyGx`U>e(tIGz6`Xdc@|ZQ; z+#bu)6={r$>P9Ein-2TYPd*kB2Z0kpqr88}qoMax29M=L96k_9jz3YhXH7hwY}cAoPd8M@xUCsa z9@eCk6r8jyRcfwVf1Qy@GkbDgoes+~9MB+X5HKh!^?iS(7ys!df6cer+2rx|V2 z>^rkylej)KOvY&2s(@|7d!qsgqlUWCRIRy96M^B#qv$?luszGl4uDvNek>L9jj{1D~Si%YD&|MAPuFs)+hyf;wQ@It9+0JwLds zIvzb=4koD{Mb4i4ksNUe*=T1F5v-~swWf(^m)S-}H2(bh6=$VY^$7Tui}XFU1_|`= z`*OAfUW*jbLR1!!nD@mdfdbf=4G60;!7IcTcrcbC&TWX~eP-+k3caESd5mLA-I-Rj z;`=J-`&vgf`5}Da)ySLgiQ<57#0C?+3nkJ6A}7887~B}DF8k;_#VcXJcVkc2a8QrJ za}#&A%ZccZg^~@2pT5-O$pGJw^%)mTxH70A81K{Eafv{BIrQshlQ!n6q?Quv2?9_y&WzTuUevh_)$-cL3||>C1j?hqez?f>^7C zXA0(bp$)S^|C*%>AcT|Lkok%DRCj!N_B+Yf~ zz?@Omz3gB=J(HN94dLztZUY|Rv0Fz@B#N6;SrNFr#$O!MQHX+*sb9qByoYV!uca^# zxQLtwTKvS8PFA+aYrL?5PwI1rnr_WXt1RE5?r4rx0x7Ityvt{hW0^Mx?(0nN9+-T0*oC3 zP%&GhVoq3mUuvj*lc-my$dj=ha1_my{ufltD0Xm3%SWMVg)F|fXszC3hmalu_jHw0 zh$f4ClR$<*$v6VPSDlrPeRfiw!OgyzAzv_S6nnh+p$lAqLyNgE>9 z1q-zBe0^f#N1d=hxMrGC@=3)_4Nh~Bo^cZvK{en(S}3CT;!tVY`sy@sM?k6ziPc!q$Vb~f!cTq4BZwTi~vRqAiF_# ze>}PeBAs$s+#*V+(~Ez|UMD|c4lHljP?o=iIO&zg9CcKRSAx(Erl^mxB=+OfzUZ3~ zA3*^$4wWX5Qo-KOiw<5oF*ll!2|i(PY0()rSkMO2Xw$BUS0o0tC79&Mq%>+`%cdNX zIP$%9xXc?-oBW%yGkKq?%-zCLuJN9oJvKn~hTN>{;Kyh-X843eMl)tvrm3t?0FgPr z6}HnbC?j0g`cpwn=vI~}i2I&PP2 zl|wcb9pKj|1i7&Wl`6xD2;&xfe@QB0gHkV*zb5E@aar&_1r4c}q7=7&oFHW-d1J}G zKf9L3Fv|a=ES)M)_9R?y>ARyHJ{pr^ ziVz6|eMq=W^sw`7pZG%jAUi2%1#vJ|K*SmaOy45S$h<(8Z_|D0mm4MTT9S)XC;50W zRYs_-Rg4K?>Vld4vai^^TWbczHc>{X3%)xQ&)$tt-saI`CU;c@r&wAIRgMAOwkw3&+1 zOvs@%Owf)7%bx0>;L)DsXNu$kyv3Ww-M8>bivB7HPta=$jp*+#|VqhH(jy!wJATZlT6})*5L2+$&2>J2` z3jy)=cHFs_@Nv;;?PVoHQ?qL{mAIP(F&65Rq82Pe3)UACNrb?WxARxQGr}`m?s#`h8^9qbF#pCGjPiJs}CR3I|60#zn|E@wU42#xJJLx_^`DtEnnbQ_8VZydq zTaSSqs=Df9$U}=wPHTE6s)0rE+hrqKObun(#JHf2Ki4drsk0L;M4*Uy+Gg>t8E3OE zPGUZM8)=2;kq?XUNa-*!#Lx=u48rVyIy8N7NcMz_=ROhy+Yq2p)cLhLqyiE+tCajZ5S+j>o|32&0!@bt8ke_C>8haqA-lmuOj~5Ll;5u*<;W+T!9;le@w>^9fL_^$Yd8( zha6qk%+lPYHrv(zcJ+6Kkj*twoG>ne*f-q|lScbpo1!2ZAu|cZkB6v0cd`}PD>pKx z%&6U=$Z+S)MNH0YmS;Nf3PLjHK^WHyzu0zXxnmI&&cwkgoQ!Xe#b{H;rt_;imz+@G zmdX#TNjTApq+ro;U}Q$Ao%RV9*b<)?Pps`luf5uSy?g>3`O}nT2)!?WN!I~a+K;Z2 zW+x28PE3g54w7jmZO?{zxP^wn%SDGsIHwQktU=o1%|iaH2v0-H%S@=g@SInjLPwjt zfJk^H#-E9H-w$hPAUiKI7@~nEUH*L#CK#T50v(JJEJ~XyJ0%l|%i#FjqWg1>iJx=$ zsw^kzHp4V*y%lE@{;vk)7zj9Ke_-@|Me;~MPURu~qxP`j)cex6q@XP=YbKdyybw|A z<5$X1sPkbjSPx8;?T*jiZk1=jSbBV*L<%LU@MzUt^%8-^++i*V)BTbhjZdo@hwyFC zgmr^i`sxIb|(`yrsnPt~)V6D;&G)$PU`7P*T+j?dE9?OSn6@>?neykE1j}4H&Hxr? zW}YLpS_hIB%$M}CHbhPM@*Z|=Y;)Wj)8QsmdFn7aQTd}wMQ!Lmfy^~c& z$42yLQD)nlDm)~R%_iLVkCs141(bB2UB;a6CPLeqJQzfd1=8I`_>P9C)rC;x`#xn0 zR~rRJ05)LglZx5EYk#VUBahPQQaHr#7kMnGypOan3Gn3Dp5#B+*2Jl#g+y*0k*o}9 z%5$w+Nx8Yi>lP2AdNuNPTLnYY@T+$;?+-xuFF*X1A1WPM&2D*p#icep)W7gaSEz^i zlO>8S{vo>4kYbWcla3VvfY$oq?wwrLIO!39BgT5Bfetq*h!Urfuv!d4>}UD|hz z4?Rd=?qKP8v;9;77t3Qo{DQKsHj~Vtic2(*K6}lJL?23Jx%U6_eqt%XiaA;+6?+=P zOp@(sFG6Uz{`@gIpffCKYvt}bCe4*o8iP?M`hqR146}ro61he`$sqkM#spIR-h(@+ zVFIAqMi^5loBQN95V`CKg8S_E((PAhYUl>O=;m8#a}Sk*m18UJl{3W|;;=o2UlM6N zWjyu>buA);CW9~JEZ36#<^%6BuJdzqALD-At4C=5V!aOUr}^RoQ6J_YxXw`0YX%`% zKbjva{O6T*3yPubS1R zMP95w$#7wniUTY}2zMw_v`2`Kdz(MN5mf4|O*fD>FI;k9Pn+BUwG?y4PrJ~WU zti2zaqVyT(P(l~t&_u+S-27zuL1Ku&KqX-pE><1|?=*8cs-YA4%HG)uH*9shDciNw z+cLpUd1Irn=>UJG^t>3%FpTNp30-v8T%=Aer1*=jQ+Y5R$(z-X=7qYrVr*~$Izg!*I)3WRL>{$J|gs}oAS;36xOA(qn=um%AT0ZQHvCI)L z^cs+}EkvB!AsQs+nVE7ZohHe_zWjhR9(WwQ@_iS{%tjP;rWba;FhZR&{{r{HtT4Wo z`#tCV*i6JH>!|o{e}BeaG-#a>NG#QfclRn=qM*bEG(w`|O)Sr#a)tq_p%;^aOwi%) z=4XMYvBm?@)&cdt4IVXr4GyS}1Wk*&`&|>5*VoivZ!ROu>j2t%bRn_Hu;6?MsIo$7 zHgK%J+D=JI9=I3+;d@|aFQ4O+IdzS@PZ37{ysHj!O^i*#x%us9C!_KAiYXj#)5qn)Sm-z6Ot8q!>YT1)la@ih-XK>NyZ{zwO z3W(}5KT79X+D)g&B8WihY$5T-GY7_J0PFq)mimNgZnwQ$`fx0%SZr#oyh<=ADaB_6^#&i$^BMIkFTq&lEQ2xjAQv=@Dc7eue;2QANV+I=|FZEIH5%U5LRQ07s1VGjNX=)-8%r_&jKSVp^Fb zQsCev=UOXHz{dF&aNjy5wg^yvVI=X3Y;xLb)vg6#Dwe>(0=9)bwB2`K5Dk1qyV6*V z5s)P|`stWS3RsnKV_IS4LN7qGJr?JVYUVV(t3TujZv0afOKP&(0>6k3~Pa_-2eTsYwXa%`;EisF*7D`Nn0OMd$coSV)}wfBjb)NbqT;^2KYA4G=9-6CZj%9@a~ znhtYV&tN0bl$J1pO(6#2v>*aMN2m9<|qMDmGNz$t?gluafZ zOC5%-U1MsU4fVmVh^Wfyaj*qCw>Co4*9ehEXTw%O^fvDF6R3Cg2%JG@Y9p)^>}~Xy zx~g3K7j z?ti(<=F4Ytg-K5|jKaGM$|`~uWnQz9VyTtPLwFVKgkdKHLOtBu9ZlhT7j%+O?Z}EP z?5V;b?f%$zG;QUc*fT*W*R^)n8Bcro5t|;4>xK_fOJwrVi_Ef6?<*mNnBjueq0UXt zHKWNtLyO^FcLtP}xa}(cqULak)ocEY0e(*n4d#p1w@#Tv@nWS~Af3~#kMHJnhFEacbRl3{ zp&CT7bSKCYUCr_hBP$$WpR!w#Iyad18Is6%Wrla|`OQsX65IF{grtmuzMZAk6nLCodIUg}XOwJK1F^(@6CCk-p%_k(!#?`MS~Vr-qil{%4uYs&qm@*-URw{EfJ9ZeiOHcv?P>U;%kPpf={^ z4sKwJ&3)hL&1Z9P?M$cnYa({K({i!)^Siu#F)t-^4a8F3DpWkaZqkpH$JJN{ zR}EQF%HR)-P>6NPEEM~7iRVVp62`+ziY+$0r~2c}l|_C~#6pFecM?BGpYLfIjmJ4% zqF*KZ{c2i=vwo*E4O}cW1w^O3N*`E0^mBIKBn@Mn;YLj+>riA9c0 zgz20I#oT;vO>R^}v%=pD{@E`iiy%mD0|W0aT|*j!+7M+QhhOq!Sq3RwWhE}vud}ff z{$n^syfk^ra@3mbi0>x(-N70d1_?4cd;9o>H*|3%X`_~+F;Z{M+PyRnTXP0}Q7 z)Fh2OC&JM1*=Im~8i}C`a+jN3xYVKHl%1g*7 z-x~b=<{10$LVsv4>56!?V8Wpm!fO4c8GZpM`z|GasUbcpw%VzqJdM31 zWYC7D*fr50#P|kPBZFFHwEZU-LCtzReU{OP#Apr4O}0 zZLY~als7VXiu!y1nV6bj<%QO5BLoiSecm1(e+Y-rGD6~gIpER9C?*?(sz{!IeMBlamPdy**-As{D}px?boTP*O*?n`L(G?N{lUc#f< zk6`uklKx+=x@Lc+A!H$-Hv3{ORMF=iAtDBCfq-nCR-{?Tw6iOXZHWZ8JzX#5aNn8~kaglHWK|kRISP zpkk`1U7svN+X_I=)KxdwD``t|a%TNt&uFz~65^IcgVr4Ke?3VMQ)&~$$8?}`@>$GV zQI30;{CkfliAK-ZT(bV8-eG}^rLvAl7k$(3<8kcKWpZvDoIN_W@IqkvAx#MqZ;BY{ z7wZ8=Y&LlQjlZ9pikzn&C_{Il{|rlrqdtgY_XZz)No)DwA3+DBQ6CMr9NT?Z)72rl zN2S#6r(?~JHzI0_`e){TtA2I)J`*2vUPL;HQQ6Rh(v zT_4J!3%?jjnTk1Y=CxBc`cVkpU!o9QXaTvSacpR(!O&jQXbl{<1{|Ul@nF$z(uXhp z-mI(bifUF#3~D&HRGdRCoboWq5|vgrrl!8_f3Y8Ff56tX2oRser)24fiu&grI=C4QJM^uf`?*Fdv~ORyD+HG~CGwNNg3L4p|e z9g>gfWo%J(zWcP0*=m6O+sq|Z@+j*aPqj+TY1(RsORZ)wW67B?S~SsCb~mx(1lJQmpu6&`Mv#J?c7i-L zf-^ep=0+adZ}e=zPhkk3dY2g#07R$!w;7+LsLqm%75w2IbR;AwagztfeMIPjNk6@i zG<7}OZ=N5=pmLL>@Fy?597YD_{TT6JGVSVkh-kK813Ea2Hgu8sSMr-{XtLZL{^jB~{plhCHKOTez?p!@IMYbL@ zqBw?(eW{Q+Fodg=Cyft5Gbotz6=x4*16X!~)}3j<{vaVd!Tg zVz^S6OXiO4kbCHiAIW$bmcjghQwH@UK#-Ou>ZcLX?Mjqrt!&C7$0OK2uZjRzyf7I0 zEdn99uag~jrCwKSVFqY25#yL zAjv{vQW2-+NGKFhsuH_Ct8|et_*8XR2Fa?2Qmcs@l9K~Y^K#ererzV=e08di{Ji83 z9mZd)9>^hCNv*iu6!&7+Mq#O4@F7HCK*g8q$Ds#_@EmacnE?2?;ZCzI+-oy3H-~iz z-H~I6#4I^Y&ld5u+z#42alxS5igd@F?P*BRJL$?=j-2C=dizPNRs^wdYdJr0vsU#uuGcwM8tyR?HE!o8xWijE%^OixVp zs)_IX$UZ!5ANm_Ir+mG8uKrHd;*|V881Sb|{8C9>YiLd}zDO;mv-{eS6UmOdQO+Oi z#6T|k@yk=~k&I=Izinr5a)tmzx23JJvu{PQ$ndYGBRi!9xHaM}b$b}h$ORFtjwspJ zKWI6+{MMegA(m__L~IwroR0p06PxP;ljZuDA|Chov{Fmu%*|A?2ePN&4pB1S^`K?N z`d&uP&*v!$bQdo)kqM!n9Z)lWyY81>C6__4B_J&4OO*_z=IUBuP_{<;<$LE?k4szZ zzPvC-7AX*-{;F#qu8ig;@1kY)aO+>l(M_9jJ};K&Q%2An0ocKa5yAVT7M(^0B63n? z;IfrWCgm#~$R=lZmzr_>Mntvt*Vql1b5h`f<--N?w46!+Jho2;f<2)XUE&Lmh5Vz*8(Y^G%5cz%AQM3 z5;w|Hx-yI=ThGHO8TiGaXr#@}rBH+gTa%<)>chF*jVd5A4Yb9u+8CZqI87Ws=or|E zL=6oL%|)II0OaW0wz`B51P<0F(h0Q~BvJU|5{* z{%!w?IQ2fU`EzuRZQ^Vpg8e`|<_Nr;RGw*?g+@C+qT-Ud8WFXTE63w0vABRrY_1u# z@^$&TYwFXriE4k5RneJlGx5AMWL{*{n#hkwC-VuU6vO$fR}}ktpEV=jf{9FZCCNT+ z7jIITumJK!&5=4h=XP+wXU&m#49wfewcBQ&`R~DLKQ9ET4ac|mpMOpgIz1oaW$nVY z0DNUMvOWKX9S@f&dJ)!^3oTGIlZkqTv@6pr)T}}vqzU(3KrL8j8?aYyfPV@DD|WQv z;ed@xGhtS<;vP72YN(!k9%bc$_P(q=hURnOBCy>^YgO{iEW4*ReB7=? zbOEw`CZrfo50%Mh#VovbZ%|d88l3jFQEE#LxwUdLxB8nrl-o0t zQT1g#BNif>@3VSS1QHIXzc>1`=B_S;KBKjb-_&#?x#-gF5VMceDcppra|3deU#Gf_ z@9KC;-ZaNmgmc&->##Z_Biqm~Ex)qp@Avis5WwaQSWv1WSPKq>+nBDMW;ysGj_cZ9 zexicr@Wi@hEP+92BTdM&-(1otwk#KL5#l^NLMac_q!%Th#J?fMh3TpF-RvUVmX+`) zdC8h8Ud#4gau~VlUp2vW+?i*`B+X`5wuSSJ`tDnl^!9#ULgu{#+*|ZtBBc;rGUh#d zbrU2t_V%3P07s*OrYja_ek6|*4}*Y)p|ACC!$fvQ813X_AtbEu4-|WdVh%xvldFCB zr4v)=P7>%EWfIxl^GC@RA1GzprC?~CggASVHROAd$!FILjZmyr^_RZ|jRMemRaG&Z zDq(inQE1>ys3+qCOk^cUl9=5j8@RXFPdymL2?K;XzX}cZHatY(Wq{(Dh@t85f$IxE zQGu-3+*3y|r_l2JimB;nFo1bD<#A{Pzvj=`nxz&mVkTBn8BAJ93S!To@NJ|LZk0^P zXG%Vg!f)!@5w?F+#>E7iwrjGIzDWSSqzQlrNHtiW^;x=hH4T(zNmFPNP_{-Ti^@u; zM)sJc{E5Iz({AYQ9SLANr`*PT?uKpyfUD+*p*(hIYRr}LoNTKO z!`6*Ku8OTl=<{+++s6-J#qL1igQ86g)`?tJyM)Tr2&$&ew|hI#FyJ|9zWbDy!k=q$W@9m^LuDaWA(IZZdaQeEMLB> z(=J6Sk~L{?Y~W?jmv4#pPwNf#VLvkx<%C8?H&;=`Q6U>a+U3Kmfnfhe4>o?dxD z22&TmEv7?9<6-XMjd;j&z}O{cui9|kQni=i|nV} z_;YLc8-QL$d4eX4Uy;oa$EXlMNE*qDoFt7EPb=eXDKNuhgB6wR9}{Nc;h04n zwzoBEV^^EvNV1Kp+|!#r(}Py#V!^@*=Z#*-85zmPgKz?NxIe!<2v%U$6Cm#Mr`kdH zHxXs{%SBLpFl+0`_tjF2*UiMLV@`gR>C34te0n^by~n&u8LmD)p%GIWR1Zs{5UF30 zMOK-1`&AKA2r-vbq0?y)geRBbFouU_`rZfn_)J4q+%X2%4!$g?Zpjwfd5;KP4a(wZ zm)@Gu#UXq5{kaIIp{;dyt?=$0=`4h_jfT`-bZnLqR6P9g3%Q&vHh0=jm*W|*?n}zj z*ot?UYNfzMkhB45uRg3~!F znQD&7aAdZ%<#yuIdlIZ5aRhO0Eg%B8-tdu2H~k?(R5C?-YT4^%uK5InrKpo{Xh(4% zrj~q*vb+e!gjQ}oCoYsNaxcA2#srVa(7_zslEucQv2-I5kM^IFWR% z3rL{(^!T5cyarwDYlpU;AS~C`GR;U#^Q*AYk%Hk?&?D%y53Mk}y;+7Di@p@x`euX5 z`ZtFwA`^sJ<)pTxIeXdmmWfv$e*-tXf(O2$TnoakpYuwx5J!FOO@MIezlS3Szi8)j z-agp_c}3$;#$aRmM$3X5hD;a=uh`sJSD7>G zNwA>eOZPs$Y96_04%VCTYmQU!M8#heZL(Cyb`e!AiN5;wo3O!MF`ccKAJ6KHNLOht zWR}?N51Sw_;+L%lAKQCvc9mfrQtU;W0ZJAu)=bq)m)fDKJ@;L)Yb9A&#pxlp9JB2PsP!klRMW$Fob#h-_StB zdw43f>hWX?gQ`2c=uwBSW0o3*jg)Agb-oHu2qC1TW5&$yiHR)rU2Gp0>9#KmJ+)lsl`7hjQ5!k! zZd>(WT1RM70(Kt1cUgY^Lc3d8kE2bNj&(b1WuaUO0I`UjUxi9M&zNGF{?(F)y-bwDFXpY;N@bWPX?kU62d_U40@e+Icy zQ?~4k71%cc%N4=y=~(G+E5!B7M+|MsH@#9}=l6kIzidPo-I-3{l(zWaHEuOGts2(6^S33cw zmwdPe5edN^)e``SFoaRai+Wm>hU6t@T{CoFq(9H|Job zo`qs|m1}V2=EcjSXe|$*--bpcU{=Q8#1OO}ctv1y%}76S0E{u( zMACYWb%oepV?G_@`P(17uzkmg1`=LJt$9V$uEYDw#D9^%!5d*AkN58D%f>BVzF9{b zRVr4uE5dcU-hTWSuoR-xA?Ca%uTS2IUJWPd>jSi2WPLM?!TroaD@=JQGkYzA!Fjt{ z%LJ=GyBG8KBa7jJop3I*@=!`~*#r-DUjK%-o_WqW8jL$vTG_(2lbuUuksV?yZ3A+{ zqVzweZDCe z+Hq}f&pm{j;aezs3fuZLN7tGL(A=>q)K@}AWdmc{d*4?hI!rT+=8Hzu|`Xk)EL|WxU(dcx5mytVvwwOR!g$ulziVhQINbu{Q$mGL2*e z^?jqQRkhIty(fa0519Ksxh($@xxRP>=ASsEn770$QGb0Jh@3g>^h)jF3T7llI1YI} z;LOx!WXzFP^QmyIzBdQ*g0uSTo%yw{ZP=vg6KfRW)46zWAiqJNyP^9xSpGNs0?u6QRG+ZbwAt&2GBuz(R>tBJ%BY$vc~6oTgs2oM=# zXEH=Yct)rpa(=f5Q2g95W6GEYsouUx17x%)w!x&sKh#U4r^!Zc&p#rdFaBMTBQzLv zl!95Vp?V@(qG?(~hL6Tuc~iH#Shlc&b}@l3H<@CT(V|5ySo*P%dLEmdrL8q4gC+b` z27<1pm4Tv$Rjk?Mp;Vq5Lv~eZjLmRjJNJBj;YnT?vWyo-wAODpAV(uMBxr=^(HXnJ zx9&e0ifyLc_Ey#|8e^MUS|P()l1Ak4kocdlglY{926=wgLirLEU`Q*m3U=>bo_Su_ zeG~%9sE0RFR$o)*5h?ItUyzJWpM@&|jE4&Dl$nr+p6E?`Ih z8b^SHhIfi-mQ_}Z&!dBfE@vMl!IrTt+Je*c?iFo-@c>LN{hfAt|H~x6;NTij(l5%d zcho^{wbw3_z93`9QpKOG7n-EQ{vvV~f8$)PX6!C_#q(tSt=)jbjixv@l+_pd4SFTU zU8MW`vLnwiidpUIc&38=9MEA~Fr zg3+Fh&Q%+KbhiHoyg!%$72p8(>+?L?cr9q}wbeS!{~bnTRPQkcoU=yFPkd|fB@e9z z?i|~0o?29BoQOiYj)x}N{aUVK6DwPT)#L+7UXPHP3QP@Yr(ek0nm1-Oyz6W$cRH)w zil1a+D)d=5cu8PIvHcNN2SxadTwec~6|fVRF1>iuE}wg!tv`YJgT8tqP#?iA$GH2n zU|g8!3fQtEQCjGa>mw0?JPT@3i-o@Y-kzt07t851C+h}ZSIpukLT;S4@?|*GVuJ=4 zdJo?htpHHL%CRFFn@g07^WbxPv3CP@s?ylDa^18C#JpYv)tl2M7GizX9pDgcWxOEVm&s`o^%c{!*=k(EB ze~}mt#G0)orKSYz=Zz_)r?z=uixPLeg~L$tVP8~PjmEtM+o}Q5dCSKzXkw~@g^l{? zV7Z0l?A@BWE+?W|-Pt;t=2O@T=NZ6&Y^oa_(Hv}QqYqf*(>`4Np)#GGg4nujGpgX| z{PiNWaUR3dA3(N;jTr}l=@bAEh#&oj@Apkp+%#^mImnYp#}Q`J2GE2&;^;T|L@`&D zoHP|F43fL%Fc6;%U~sa5D5WLU7dd$+0Cgs78&<{UX6$#Ot!vqxPJs2AGajwc`+j_l zHt^!c86UbiBwAj&fNPBx-*(-0XV)8k`bt9p^g1_wI6^Lkt{0$AQ%r5=Z1H3?|QA#yUVuPn=J!s@r7=ia(ER8RYpgrsp0v=_wDKtGxH z*~Y9UXNxJXy%}`m4qtT*$IaTxP3o1-oB{g4A9mSKElq?wV*Kr)2ZQZz6qYlN%XZLk z7eGrb;)obF6A10ajt_vN_pfGZ0wCoD0bRQc;4VOGxNf(iNclBh5=2oZ9HXufylG)W z0c58>5D`_b#X>`shvVz@3qU07X7*8XQHbiHH}&89xOu!DX9z5DRTAERB0eX-QGZ-} z3^_fdkYWCxF81hmS6THq^XKu$3Rf;>kvK=4wE!`kPlEP-2{J{43WKNcy8;fRD!2%c+bSZD!(M0C_!Bt!n z8UhqUzhz{!_fw+X81GU?xgfz*Xl`qwjR9Ct%z~;z(Lo>10V6DQ?a0~J_Q!Rk0}sGy z8yDftm^-;ER~3=83{Lf+a`o5v`NSPPtV zxH9%NT^Rg{(z(s-cC}(2O07AYz5*x{E#hMWAj4_EETr)tDF2-bI$sB8Qk%KaUg@bE z@~tP#T};4MVoK9N$|#71?mH{5tz^}-Cz!blgp{3BRaLqAj95|U)hy_A>MY!NeiT`fHhg}YcSa}bl9gjRaRG0CD}wGfS5?QmOs=v`B=}Fx z_&zpj%CMjO^dQ*KD*4(F17dxRpbKIPB;sw|nCxA{uNWt(Al97smH8#XsBhkZ~GeYyn)#dz4HDq8Y=86B@`Dt&F4Py+AP$TE8XTw z>c2&SWiiknittjgWc(GsbQ)&+8LK7S0t2cFAHqOGD6wo*4G{}vrcQKT+px2xwy4h> z!NK5~(D_+@!cB}-ltW+UU;0UX?@wN!?N5Tj7PGdxtH9qGGNGiEL`Ff2)qfziNW4Ce z5Ed2nWBy6zYxM6e37l1ESjZ!1{I1SeUz9PDSvA4>BP=4vN?v&Y5m3gu!t=g6r<5uE zWbw~7s)35WVysp-$Hmp7y^I}5BS^9$KB{t{V&}eV(XJ7yC(ar}F;!5IwG47-{Llwp zzldKEgAuSyb7|VipMIJJZFY;Eqk-Cr&sDWLc&CBelV7+seUQI=W28>P-q_fL(2I~N zfb5YjMAk)2v!&$xG$B7F6IRVl&t!-peZ&JUs=E@QWprdT%|@v6#x8@PF27Ipb(z*j zQzEqKOo)=2rx_3vVu@MaJ$rSnij-emOKSa{Iox_O#tWiO$HJ*a1U6eMEd_J)TZT0P zkx^R>uT6}Kdo^odxc~un;v!l+#h%hmY)_WgTO}$Kh;1+_cZ|fI{h;f9IB?~SvGJBS z0~;3@DLTQW;-q{${N@Aync!sQ$~YcivWtObwL7^8I?)M%Akt2x-NPIy(OCOSBsO4V z>(Nd?EjL`68Q3zuCg1Ddz>-~F^-5I|7D=Tb1`hu7bnqVFmZ;&P(#}5 z1zp5#?aWcQ6Hcn`$u!Vdv-=~c@_`sI43F9X;RZL~%n2I^WKkN9lPxii!*RD((UO^l znWw9gfzG{A4azpE+C^)B7m(I7@+-lAn(8zWFYzOn z^UFv;sD*9=>W*^8Uv>Hv4Nee1Mw4OTTu^{%zR%bF$U-lASa2Ggcf=@ydzrg8=YaxU z5})<9No!sHe!J3y@tQ?!3OcJ@5bj4nx1w+*Xf8bj{N>PeaG#Zjp6L%Yup-lZj)zUt z^6{edNmqPcZ2n-2DN>kUUR~r>Cx)?V=QmQY6soc72y3%eF_kWIy!L$k1;aKM>6*IR z>@+Oia6KyrUnSFnFxzfqOSE9mzdPX^L`Jo)Z1M`mKjfq0EH17~!s`%sLOzWf; zVA;#hOxGlvi+9%*9|qN`o;6r8<9+kkB63&P(lj|;HSk_Zaga<9AcnMISSVZ+=WvcT zs4r^2s|bS5!9!7?m?5mXg}fe+}Lz0#_`r@#i9- zco<^ag)pUr=yLN{3lMYA>;FkYUJ`SF<=17c3}ITs(L@*RPFhFDw7oT?KQt2hC{sYd z)-hW2k;z5K6cIMPsH2T3;XC@5DxS(YoB5DQ=jvV#xYb!gEIE6iKcvcJPd2dXh!u}o zS$3%QYB=lQJ*YL(5ax7NCp8&8##id z8)-L)GM~j`;Ood~oD)GXm<0T=YF6%*ei+9rg>DR0lr2ap%Y1P-B!b#Qs%G#_$2{o5 zJ|U@9gp6@IwM%zruI<{hS6J&ZBf#M7=ue!=8M{Jo>{0@!>XlW$cqQttURYT;76FFC z;u^_gV&-sN;;V;m(`aTi;0u*G)RLQ;yO#iE;xsV3U9EeXNgX3FzOqqPqh* z#Fojbz7yPnW|4%8IMDSEF=Tv)K{DW;pHvfv@mq=)eEWn=V@`F|_fgIFYKZxCEP~cV zy1ZsMqsru`2+ky!pB_h|i*Q|pLmm1#qoD@Pcy=dfOj09-xrBtscCdmT6V-tk z$F0;My3MVrmTi$(eG~OozYY%N5F@mK_+JIuvHL8hoEJ9@PVig#_(G5uJ!!a{zz9Uo z0tx%A*51jDFaeRqwOVNh(o; ztnD*ZMa_QuW_>>JDBzvLvT_?>gI_fMczj!Xyv5j_xP)L^81`Q#s+?J!$WMHI4*NI3 zX$rh8bZ4M9rmLpl;VIftI~>&@W>fh6^e8x*qdn3C4o-_?OeS(aBG!t;p7UVQq`VOs zv+*niWc;6YIGTCQIfT_=ufENv7xF*2Tx-$T^%Yb!LSbXJb6cfXOiym$VpadqeJ4$| z$QBP(;TXacX8QxzU4D0i#zHsBaZgizbd$;$$f>Un-adNg-036Zq38M|)*xmF43=D- zL`2>LEzOIvPo1RLwdUihS59Ha1>s=zT@w_{=NVcDl%x@x?_g%PuNwstQ(OhoqoF{&Ln%hjH%tF^Xy!9)1Dz}-*$73 z*TgF0l?D6e<$9I5ts_TF8lcH1{R>ryBetk3Qw^X^9cs7c!o&fE*F{2=GPLO&MBHTW z%FAb>n_fVD(bBOAS3R-;;Lg)QLQsLnr0~*yv|(E=g<3*7y2_io7scAzp`?7FCg~rc z^t^(esohQF)pB@L&^4XQ##7&YzrYrX220we{VT zVb0V0whUB|n$+$xHkP77D}ZzUDvODPyfjBP2t?{RFsTJA4KC8umBdFzj_a4baAGN-UVLC40W~2&-Iz*f0@|B*w%N--@O&5Zssv zyb#Fn&qYUUs;5;{u^e^TXOt`vIA|7-18wdPLOY$$g#F-*?PjKRu{T zlFG1LeL@1-;N$`gwnKhT(IP>#z@qnp0i%Rq)N4op*;#WuZ`Gu$%&RM%4p`G0BFJ$U zKa5IIY!m+x0GoDGDr$0O2l7Q^(?w_8ap(S<_ynSHSplZY#b}=$iYmIIv)*)(AA$O@ zuXgqW4}IpP*Ps0^^Y{4jPePC1x&P33fu(Xb?01RUTe^Mfaj)JyyKEop*Y*rRPBu_f zS=-`5%@Du;>kj8@WynrUH<1@myVQfcL(I<{aq8}*jSkf>LyU8+xszjA1J0e=wA!lZJxbiXpl)?znqYRtZLq7Vv| z)IiB{nrJUK;n`=+qp?)WI+!=Tr>+cAPmF(MH#lJgnfJ}YiXnIzeV>#$^{Jz)%mDro z%N9QSk(_rV$+=_st!ID8Be*^8Pw6#st83<`T;8$mWEPK$3!#JwN=xK4@2aA4zKNW7 z3;AvDwZNLtsM+Kf*q;hdrRjMc1{pq&gsfIoyxavZu}c4z30seFuTx}u_9cC0g1)na zu!%>RjaXrEVQec9g3*>QtN1~(jzWtqSP49oYF@`3X=4s zSM?iUyKA&`OWu8xb-ED_|1QEE-4_+ekagX!UJRygs1!VZPwYeGRPG3!HVM6LTw50M zIwu(*@bpk&d_#x<;? zp0`cT!G>55&)at+>J_2zYRsv-Z%Tq^g`7vbc1W9h9_gj9s? z`)~gI{jgLrA2^`Pvs36$O^d(%+Y`NYpf#xsu%U1iOsbqM9sS;Fo}k>-!;mpVb^3Am zx_d!V^2$5{0nBF&PwQ3la&b2mGif0uF?fTNClSeqo897Q#Gl-UfWM}T81@ZoFj2{^J0{BSP6-sF#2upxlUJ)mt$-xbGXi1~v^I%^ixR zidK(QGg{yH--lCMJAnwzMk3vzwM%l)83VB}k78Oo85gc|fBhwGrZxwE(5l{C5?B|9 znA_)A<-w>lc`&-f3u&e)xc}P-K_WFTAkS2hKjx+Rj$*6skOdQziRa3c{OYXJ?Aaxh z!A~)qti{IcMbtU$9aNJFU65>=*QcjH1k{ir$9;KP)q>`kTMrN z#Yw!`@{)(yvPsnp^9XE>0=W4_if6xXtxgti<>YYA$q9$vKz^7LMEfJttY1k)MG)h@ma?2*lYYGMSP z58!-1{ zdg$`pK19xo@mYgxv0TOJQQ67$ymO(Ie@|+3BwrayDA(iUTwf<=*v|U6{*9+S|J2o- zKEgZY$<~=$XQjtl1=`}KOs)RpAR+aeAWy@R4IOgLgh^!#p6xy+r9)Yx z+}{ZAYIRl8#%uc3zg7%`UO1A?>K$#w!hyashwQ^ir8>(}%=|aX@&~VPLwJXh%nv8` z{0C(Bb|X_#V*=c|6LHN^11{^2B)e+X&``Z0{=87~C|&2sjxj3yiJg9`jGpC&+Sv%2 zGRjXJUn04EY_rw)g>;18_W_O~3UIH#0P*opQz=t5j`!(#1H%DEjzrcS9}Te)@1pqO z5qPo*_vUdH4|3?qIWTPsNLRX2lA{IP0mGdE2U3wuVk!$)BflUzS#GGbP(Wy=IbH|a z&ilKIQj&iW0J-U|l^n@xt&G)3Hr>hGZfc9u&^2_wju1cwaG5s2Yr57E&km6pQawk3 zD<9o^f4(tTXsRm4%N`m`C^g6ao3&v67+O^(Gf-X>jL1$)6J@ z#FE|7OLoKe{2Qh|{}j4T3uhs*2FyIEAV!N9kl@6>bQskwcoCiYuKJHaQ8opuh2bDF zpEKFO-kB*FhaW-S+o0I5VWY0j7i<%*56nJus6c#ahc!b%Gus?wjuHtq?XG({$yTM^ zR{w1t;J$c;6=C%zodRuut0knQ+_+!$hTPsce!EZ zte$izAm93n(fHAwU~$}dkv_hC1Rvv7XwAK?)oQe{_+TARK#UEUkFgt59I?b7k z^bvSTtpVJ|(G6&Py9>gtjEZak>f!*hNo%tEyPjr$x~_}s_OPak|0+$v>R~49p|B4~ zF(>$EA(J7?Pu|CGP-B`nWndVxb%B%6=Ln>+vhZOwKIFDZwFam!N>kr|;Kq3Q%`wEN z6BAlECeDOu9LH<2AiK6-hPL(>rTq1VW>-6yJzm1N<15~(m^~7@AbV_MR~mkeE1XJC1&xi!?K0TJbvAT)?K$%k;g5iC_3VYU$>NRzD8cDzTCeG zz^*XspISTB118u*FgVzRDAD}?ViVf-urI#LSFSnBnP)s9V1dMBv1w&2v~e4KrXTQb z>R8t1ar(D+DAhkBvz}&(e9I&+{cZ0Yzz{G%kLrb`oVO*OUS7i?yvh7jic4iBzuFHaR zcj{{yjvI_1+~<}>QSb!oa$|Ck;;BTB%BGn?V{%;1uKc^kcO24YKCU`Hz=NZU5Gg&)Z%Yh}Npb2OqlvPASs~WkDMbr9UB=eVdas?0Yh`-?} z(t*i$sxCRnExCoX2w&bCT5b<$H?LXw9P5N*I;=%m0sKa26vb7y*34CNc)AiCtAiZ{26;k+T;DT4krM6cIE zH~?OyM~_RFG2pIxn^#I|%dzKZE+vERAD`A${jYfsRn|-K?{v{K(y(79KUEC+K zza18KJM$z=PhdR7fdT5arTx@&q4?`@On4iYje)r_R6(G8h2ihL*9+fq@TIAq zo(|2J@M45gl<4>U8vebG^CjQ$fO12{OibL|a)bGLty+w|NN4T!f}JLoLzOzH9%+ez zyhMLpu-^J5pO(((#qN+uR{YHmEqub2Ex?-@uersuuj;cso;$LfNf@5-$^OK{CY9!j zY-Qrs;=38loi6U}s^v9{v#M+{dl zmxkw7U11|6Rix&r5=BLLX6REs`^O-`l7g9n1hGvZ6Po1Iome_ z5Ag*di%V1y$8;=x1nePj0fe~oxgcCB`N25O*A7BMT{Bx9G&u1qkFr!8MEH85W;mhZ zWW~FRoi64K^k0HEn{^dskH<<7P%LwfVecv`)!Tk+oSW(8pWc@N=}&5jGv9w=D|=T| zsCWf*T{MSi-`NPlrJ)x+VYWdb=aZVb7@z)wVM8}%qo>~DG$g80XJJ0XK*77tn-q_> zB)v7{mK%nOAV6-FJoOH-J$jS}9-F*b7iH+oa zU+=GL;lY)Xo+SUNf?+(#yX_F3UPT@uvBpi@r$RC%Z0V}c9^~zUlnTqT^jVkB={At< z#sHM*{C~n3OJ1qy3+?Zmx5ulh9wOK$B-wlU?Zj5cZBRnCUDVj9_ArboZJJ?n6p{2h z={@ZoyQu`bJC`30mS)0cgWsc2?XjeC=(AeTdeZ-sIR+>!Ls@JeQ#syBR0Tl8f(>wB zD&3F_zcpezhCTm70dU>DKK0H}OHFwA1f{lFlwVe;Cd%nEhwTyU1@TeHswmd2?kF1G z(yT>f-{AZQ@JyR*ju)5DmJ!IbGCU%7X=lw&Vuwppp8oux;oqbhOtW?A0}aKea87O1 ze$*c)5Gh1U!$z@bmRAH(z4BrF&VLHrOhzd}NtN#5$W2vOdkLg+e!XcuQ zrnmCb{T(Zxg`}2^OH4Pxu^S@{>-*`i3xZ?=+l>=4Vn8^=r|>W(*o=PCN0Q8W2z@&I zzT{9Mqy_4K+%Om^ju#b&IH)kD7hb-+ z|8l_(!)uC2PHhZ!GUC-w_xt`VXdCDey}LLLsw2Makjxny$;$EY!HBMx_INxYnR>S_ zeif&Wqho~mf0Kh%4*-Ul>dEVaiuBsvYD$fO@zD= zBmTUPsqQTGD_8SFJLk^7OLcrw3g^5)c}2?jAkHQqF4T@A+`#Jmj!a#4D*$O_1<92Ka8UcG1*lclRF&8+H=2cZ_0=B-?jfz9FTQsL?;^ zG2UD@ku!-gi6dk!OH0yUFj$CvgjRnPNyfLu+=;0|xpp-~mm6W!oHx#VZJv(FXy37K zWlW{77FQFG=Ypf=V$oui3s%gdt$=p? z@B%iMmz02F5!e!I+5&9wzDku@gwIwo@SPBcezULF+Ac7w1rWo$xo$LG?;zWwa8V-| zSww`Awo_-1ej>KmpGYG{2upWtEd||ZRK20QFV)@Yw3r(9NqmMELyWg%6Bqoy>O3z{ zLBDq6Fbpg2UxKM@D2fxa=`8k-O6<1eq9)MH2yh+LXru8sj!^1C-T&?auZgjPV4+0U zmKPv`wjHX{11s3!>-LW9r*ILSbK;nLEYc@RB=Y!sLnZAldHO8PR z@z%rW$RxAN|JT}ewl&pk(I_Zgf(S$-fvB_uDWcLr14xnfA~jS6L3;J21`vY;kfKtQ z(3?mJ1PCa-3etP?-My2dG1I)a`R*FX!X4n!~iFA;h%!GH8Sb1xT3Sfw<8Y2U(7tk9G>YEI@d_VOX*+Ol;u`oH_0?sRv z23m7(_*Yh9{ZQ4|v9G|gpxrZ3k!c1l>57%Ga1BbY@_u%~X*a|@hUy)Z%^p+?1`85rdh-KDR{lai3Qw+5F{ve$x zXX$?G98VLT<@{l(;FW3R;@EPcZpelZ0&1l|M>cS#(xEt9EsgS(gw8+G2tG~jGd5A0 z^7Fw~o(J4%>Ys>56dY+veD)t3C9lsp;$m!cg;g8`wiqT#ll^N83J@@h9{yK4Nt2Qp zT*;3DKQEnnSphgMY@N~ZGT_ibdL4@tuH=lu%Nr%~XYmnGjCDNQVDW!57jB&6Hk*c`y!bu(o79UBvq`E(!D8x@6qdSxtZMxp_ zT>ML+Xvz;p7p{9e5IZ=mx0F`5Xnv(NqF4-dQw)2#f5#O(w7|N{S(Qpj!&;IUzob&s}4R5^YzN+z z!ugZk(fBmQniQ1UDhlhmca07p!}~V^*<+Y0+|M;h!+7C4zzVC(OKstut?W{;nY>H= z?Y8&U3%Iq=%qsM&1|*YtFCSTi2(EmOy2p#kY$k+)^!5D}(v`OcRG+R{fzb0D7oR8t z(;)*PyevO+5^AVMKDB^H1!co`0drcO^;)8V(2+W_pSypI>h{K|*zw zVP9h;I6DSW7wOq?-hHg8^YX=X;j81j zKD04)WX)}RU?QH3T2#AM&2Rfc=X6xtc%A&v&~+61p8x%@Op~$?6YW-&35M-!q_f!* zjCC&a@>f%`<^w-W#Q_Oh`GO6R#>nK!4SYbQ3z3Hj6SS{;eb`$+?brIS6%185{I>V5>E3n_z%tXheX8jGnUflb=Rev9AY(Z@ai#nYenh@x*^%V}<20l5WWC;r z9z6}#b9A8vJ^wA8-xl6+Gf)KXnt9x4#waOLatX$Bo5=08@BVAU$F#h7M+%Pf0zC?X zA?#S`Cw2qKsr<8%sxXohJB6TI#+`U%Qi%Y(%oh?X?;mI^qGP^cfSOi<(2Bq55Cg}Y)4Cz<>)n9)~N%3IBA zzN@UJpO@)F%ZRMVjolnsOq2*K`)SunGzntX@9y|m*6+B zk<2+{|14a$bEPOZvBkGS%d=Y!eF_tZ)aXizv$~%F5dmBgR;!PE$M zy=A6ch8Dpu1>emMhHO0Y=8Qa%dd>$w9 z=o`|%#+F=0PT)jhTfHN11xP`L^X6#>_lRkGa0hDSpz>6Aa-)SOn-(^J-1tXZ!aNjixmQQ zK2d1OW+hJxHlgt6*$7|MmH9@cpz*l1*Hc^0_33`Y!5V0t$KHmm_`vyft1zTn3azCu z=-NmAsOfx{Qo7JHbdAF45b~odhl(H}JYdi`KM+0*+wTb$dmN=vMm*hCqbrdSDKwQ9 z7sx&$nr3i5itt!s*1fCec`qp6AEsBls9&f$_Yb*$(a!CcwXWkN?G8| zh&u57#z62H764mM)+NNVdvTsR0P?Ms%szLlX;{@H6LDmgl=a)X@%q2>fedv?quuqY zn&K$WMGt3R2C?Yx($*G3KB;1~_lWPaBk}|Dj&9!gaxn@-xjrQ+IwP$h_4NLu5C|N~ zS~9>yOo}&VX17boZo1V|Wu@ov(Wq6^s;{3OtSXd*xRZ)pntMK%tzNki7545NtY1)e zJyAGd3d3kNCb5**6V;t~{pfrDj`uvD1~NQH=lC(!mnz{rL0ppdD{e0R{>GtLQ?BW$83b`)&u; zHk3&P{DvOlEpCr83y@v|q{`VF}oYJf@2Ec-FS8$s8xXv z2Fx@4NQ?1M&)(4LTG`f)6$ul|?vpwXDp?5oL60#7VESoblvNavg?4h5jMjkdz}s1L zvmdNC80;2w`mWtJL#OLYY3P+U@RNr?njwm4CUZKIdf4A;HP}PGdSp~r z6q#Y>tRv6N20g!XYWQo9J$BvUB-0-FMZjtVCrUd<$_JG?7Xu`5%t?M{qIW=zk9gEfw}@57QqaWHK<+I!vN*Y*s<;E*uC?juB@f4fLi4R zZR3l`eV)?}GtY3jdJYOdV2vj%f0Q$1M$r0?gL{k>hKwt1%u4?DDy)53j@#+mueXRG z|GJm!6$i{X`fa!OE)DnWIo=K9u4CyGYoAjL^ z_;&#P-vL2c-7oPWMID@{$554IYenixaPO(7CYQubv0gw*{KqnW?4}PKulvOZF15oGbGQ$6dB>vB@ Zd#48CZ1u|OmK7l2(Nxn_C7^7={sWnjNg)6L literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/pt.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/pt.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/pt.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/sv.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/sv.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/sv.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/template.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/template.xml new file mode 100644 index 0000000000000000000000000000000000000000..0acd5b1a591b8c0dc9d9d2269c9be6456096ab4c GIT binary patch literal 938018 zcmeF)XH#8Sw;k7rMvJO`gj zNvX`d1Oc40_u9*^X~rCD?f>up{dd>@>^kq->)P%5rR!7I>#n1&PbpF&EI!P`zEzK$P<0dGizOIT~E3uoBW2mhP%e| zdAw^Ne}}sM)iu~Pk-CoL(?ISGqz(T{PUE>golm`8qq*0gJ7dlN-lSG1b=^(buex6T zR-dW##M^v(pMH7Qq&Ugvz0~$N^?KRVsoK<&JO-NFdz-%lUC)x|aM%Ax>7)7EpPK#J z^y7a^Pp;uE~UT5WUvWu9oa`0Cwn?eXc1%yctD%z@!%bjC6sH9tmktv}bsG7Bd1_g@*U{yZ_6 zXNHjiu(iiPkYwg~bS^Yl$w{lP47>61a zD{nY87)t&6ntJ;-l9~*qZWGP#VD60N`&iQU=V!cW!9eEuSnjvyOsk&1ceKpZpEX+6 z=wRA1l@^R=EdG`6!)d2AT-7Co@KR!nz(y>fu{Q%d9o`-*(MsjI84S zY|>u6);Yf2De3B}`MMcU(&cMsz(muc8uziZi@X|c#<;&}x0ySTPb1Adn8>}6d>d?@ znoR1y=L{Ig=nkb7+A@)G{a1RwFW=3Dfo7JQ3nRHQ*vyL2wArl%%yZ!EuUjnkG@e+L<0-uE

+lTq!`U}1Anm@(BDYaublyd8F?+I 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 literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitAuth/zh.xml b/packages/cli-old/template/fm-addon/ProofKitAuth/zh.xml new file mode 100644 index 00000000..5df205c4 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitAuth/zh.xml @@ -0,0 +1,518 @@ + + + com.fmi.basetable.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.basetable.proofkit_auth_sessions + + + + com.fmi.basetable.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.basetable.proofkit_auth_email_verification + + + + com.fmi.basetable.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.basetable.proofkit_auth_password_reset + + + + com.fmi.basetable.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.basetable.proofkit_auth_users + + + + com.fmi.basetable.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_email_verification::code + + + + com.fmi.basetable.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_email_verification::id + + + + com.fmi.basetable.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_email_verification::email + + + + com.fmi.basetable.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.basetable.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_email_verification::id_user + + + + com.fmi.basetable.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.basetable.field.proofkit_auth_password_reset::code + + + + com.fmi.basetable.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_password_reset::id + + + + com.fmi.basetable.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_password_reset::email + + + + com.fmi.basetable.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.basetable.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.basetable.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.basetable.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.basetable.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_password_reset::id_user + + + + com.fmi.basetable.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_sessions::id + + + + com.fmi.basetable.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.basetable.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.basetable.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.basetable.field.proofkit_auth_sessions::id_user + + + + com.fmi.basetable.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.basetable.field.proofkit_auth_users::id + + + + com.fmi.basetable.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.basetable.field.proofkit_auth_users::email + + + + com.fmi.basetable.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.basetable.field.proofkit_auth_users::emailVerified + + + + com.fmi.basetable.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.basetable.field.proofkit_auth_users::password_hash + + + + com.fmi.basetable.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.basetable.field.proofkit_auth_users::username + + + + com.fmi.calculation.text.1F27E3E6452F6E3D407EC45CDFF933C3 + https://proofkit.dev/auth/fm-addon/ + https://proofkit.dev/auth/fm-addon/ + com.fmi.calculation.text.https://proofkit.dev/auth/fm-addon/ + + + + com.fmi.calculation.text.59AFA301111C185DBC5DD64F78DB356F + https://proofkit.dev + https://proofkit.dev + com.fmi.calculation.text.https://proofkit.dev + + + + com.fmi.layout.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.layout.proofkit_auth_sessions + + + + com.fmi.layout.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.layout.proofkit_auth_email_verification + + + + com.fmi.layout.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.layout.proofkit_auth_password_reset + + + + com.fmi.layout.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.layout.proofkit_auth_users + + + + com.fmi.layoutobject.text.0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.layoutobject.text.code + + + + com.fmi.layoutobject.text.1D65D2EF432DC000BD3A8B0E4DEEF346 + Session + Session + com.fmi.layoutobject.text.Session + + + + com.fmi.layoutobject.text.367E8386949124D8EAB7A725C6370BCE + User + User + com.fmi.layoutobject.text.User + + + + com.fmi.layoutobject.text.3CA8D8AD5BCF2CC9CE79B9AFA97339AC + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + com.fmi.layoutobject.text.This table stores your web users. You can customize this table with additional fields or relate it to an existing users table in your own app + + + + com.fmi.layoutobject.text.4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.layoutobject.text.id + + + + com.fmi.layoutobject.text.5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.layoutobject.text.email + + + + com.fmi.layoutobject.text.57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.layoutobject.text.expiresAt + + + + com.fmi.layoutobject.text.65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.layoutobject.text.email_verified + + + + com.fmi.layoutobject.text.67D86F2872734BED828FE6CC9AC70499 + Password Reset + Password Reset + com.fmi.layoutobject.text.Password Reset + + + + com.fmi.layoutobject.text.6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.layoutobject.text.emailVerified + + + + com.fmi.layoutobject.text.74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.layoutobject.text.expires_at + + + + com.fmi.layoutobject.text.7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.layoutobject.text.password_hash + + + + com.fmi.layoutobject.text.7FCE1B3FF9247B2EFD3EFFB225B9DC8A + Related User + Related User + com.fmi.layoutobject.text.Related User + + + + com.fmi.layoutobject.text.864D7760326E5A71EAA190E2B81A2630 + It's safe to delete this record if the verification has expired + It's safe to delete this record if the verification has expired + com.fmi.layoutobject.text.It's safe to delete this record if the verification has expired + + + + com.fmi.layoutobject.text.A2A111D0912ED62DFCF6C56D413DEE3E + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + com.fmi.layoutobject.text.This table stores active logged in sessions for your web app. If a session is expired it can be deleted. Deleting an active session will force the user to login again. + + + + com.fmi.layoutobject.text.BE53E00FB97CB96633D9264982373233 + time in milliseconds + time in milliseconds + com.fmi.layoutobject.text.time in milliseconds + + + + com.fmi.layoutobject.text.C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.layoutobject.text.id_user + + + + com.fmi.layoutobject.text.DBCCD67197DD180FF75AD2AA9FD1333D + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user needs to reset their password, this table stores the password reset code sent to their email. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.layoutobject.text.username + + + + com.fmi.layoutobject.text.F35C746B404A8FD17848FCA5A9500A15 + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + com.fmi.layoutobject.text.When a user wants to change their email, this table stores the new email address until it is verified. After verification, the record will be deleted so it's often empty + + + + com.fmi.layoutobject.text.F7B6AD0D6B71B4C978FB5A2BFC1BAAEF + Email Verifications + Email Verifications + com.fmi.layoutobject.text.Email Verifications + + + + com.fmi.layoutobject.text.F8C144711470F2FE602C612E30803932 + Learn more at proofkit.dev + Learn more at proofkit.dev + com.fmi.layoutobject.text.Learn more at proofkit.dev + + + + com.fmi.tableoccurrence.0766B2B7768E6DCDC52A6A033BCA45AD + proofkit_auth_sessions + proofkit_auth_sessions + com.fmi.tableoccurrence.proofkit_auth_sessions + + + + com.fmi.tableoccurrence.12131E1A6355305D7BDC841A925C5A56 + proofkit_auth_email_verification + proofkit_auth_email_verification + com.fmi.tableoccurrence.proofkit_auth_email_verification + + + + com.fmi.tableoccurrence.5E70A3CC1ED3EBCD700544DFF336C69A + proofkit_auth_password_reset + proofkit_auth_password_reset + com.fmi.tableoccurrence.proofkit_auth_password_reset + + + + com.fmi.tableoccurrence.C68768AAA87CA3FAB34F82AC78F568DA + proofkit_auth_users + proofkit_auth_users + com.fmi.tableoccurrence.proofkit_auth_users + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_email_verification::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::0A2A6F666A2955B3C0D398EA50924A61 + code + code + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::code + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::65E3C50A87BB3076D0E717CDAEAA8001 + email_verified + email_verified + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::email_verified + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::74434AB5FCE4FAAEFDC691DB64D55AB1 + expires_at + expires_at + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::expires_at + + + + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_password_reset::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::57A056C71AA448A69FBD9960B1053E99 + expiresAt + expiresAt + com.fmi.tableoccurrence.field.proofkit_auth_sessions::expiresAt + + + + com.fmi.tableoccurrence.field.proofkit_auth_sessions::C9E0452F2F891DD359995C99F6A2D0E3 + id_user + id_user + com.fmi.tableoccurrence.field.proofkit_auth_sessions::id_user + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::4B68129F6621C41900B27BF59AB8FD9B + id + id + com.fmi.tableoccurrence.field.proofkit_auth_users::id + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::5588ADDA2E7F62A48B84279D69752C99 + email + email + com.fmi.tableoccurrence.field.proofkit_auth_users::email + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::6C65589BF319743648F1CAB95738F7B3 + emailVerified + emailVerified + com.fmi.tableoccurrence.field.proofkit_auth_users::emailVerified + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::7CBBAD414D9185890C6AE6EA4AE96E5C + password_hash + password_hash + com.fmi.tableoccurrence.field.proofkit_auth_users::password_hash + + + + com.fmi.tableoccurrence.field.proofkit_auth_users::DD6F8C0A5163A91CBDCAAFEC3DB91266 + username + username + com.fmi.tableoccurrence.field.proofkit_auth_users::username + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/de.xml b/packages/cli-old/template/fm-addon/ProofKitWV/de.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/de.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/en.xml b/packages/cli-old/template/fm-addon/ProofKitWV/en.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/en.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/es.xml b/packages/cli-old/template/fm-addon/ProofKitWV/es.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/es.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/fr.xml b/packages/cli-old/template/fm-addon/ProofKitWV/fr.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/fr.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/icon.png b/packages/cli-old/template/fm-addon/ProofKitWV/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..30b542393e5e0be1f516add269f7d5618dfc3255 GIT binary patch literal 44003 zcmd?QcUV(R*EhNo2!!6d6zRS9-iv~OiXa{7JqSpLM3G(;REl&IP`ZF3HG+T$2q*|j z3q^VfJ@m7=@29=rbKdVd|DWq(1If&+S+m-#HS^o?CO7oRh?$8203b8a*S-k=Q1D+U z04D(d>;;b;0|1QP{kBDbg^{76qp!E9gOl$)XVGA91lSG$%4)#~2S-om0FHakuI@f6 zoLkN9oE+{>DxBsrM&d>YEoV1({m}c)w?c2+b`14&RB+-{QzcdoRs;!nI|n#$1bch= z_$vmhaQ;PC5&Zx1YcWoazq$l?s&HBunQ&ZvqIgqV_aB47 zPb!>l0Rad_F|nYaAkiQxQQ!NnViF1p3S#1tVv>>~U=I=h5T5{tU=bgGu1gYs)6jPI zcf9Y82yplH;kcyfaL+d|K!uYNZ0GnVXWoc^5%=-`$Lc{jhy^M*X*}I=K`nro8+Z#6%D){ zT%FC`odVqcP3_+ehR!~&|J8D-Bd@?qBRBJL2etY)3x5r=_;;HMr<|nNe@t}uznU)H zhC}%;V^Y+2_Yd&BAM)2%fBVucXTQHc{r%C){Vylt;P}gw6dfEd=U3rG-1l_~baZz5 zO9~*lzgzu%T>^p}?mKI^f>xx$so~<{4ssg8VRGqD9FiQ8py$i{&mlo>&LFw}LzDku zE@J;+P3+RI|H+%O*ne@a{8xwn2n67`zrOc}z{uQdg5B~~w&OV?S-3LRN<{Zo! z03axL&>;TH5CDMxY2ectp+}~SJ!&l3N$=-69w$=WcgTLzl`QH0>{XWJq}SGgkH^ao zhq>GzzE&1}PPSt>g-r^MU855C_`qng#&)o|^1ajGio)Ph_+Uuhw-EAiE+!t&krR%W zeQUEOLv@rkd-BtWxeg<~2#KztZWq_mH|}<+;}yYM4IZPaht&!+23ioSVr*H_O319i z?8<(N%XS{(Wm*bPv{>{>;Ku^&qt`vxPhI@pepaJjdy17GpX$iFP1jRyVX$`BAw*&7 zSAX`+w$%N!MJJ!g8}~AT2?-oD@wdgh@aORvowOGL+f&jn3Cu@r_s&Vn)5^b8)S|u- z3w&Q#VF)yO%g6J!h|k$+yyp1gLv@Vef|!{vxn^g%d*mL{k%3sL50 zo~V7s7&V=lH*QmUnBNckUMA4b#Fl%zMg6=NTERaWKVe2UJy^&?Tp2V}e|zm%-Xm1-YV0yRQ)b4n9f6)L7jRS<~G$00_KFEN;3_BmMHu&_A#2k zp0(yiG#T0)Y6rC=)}Cm`s@F!jdN5v#T-p{;XCBgxmQ`m#rK5gm%hSo<#b_5asC&@C zknnZg>Z5&hDf-&p@eF=AfmdJh0>e>IiS9c`D7sAAs+yHb(_G0FYJ;qTs!%n->j`eA zN@SVq0O77k`~;nGh_*Iiw;9y)$&exnUh}3iQHuPdf>kFn-ZiP5N7D-aSmo&iWSLu& zkVANgQU49P!HS6lJxmL+gRn!`0il+QD12>yt|J#JDZ$4?K=DnlX4&FgN@n;TL!sCZ znVab%M~pADpu+oMrM_F`vfzp%DwXmMykcYTJE;{9({q>aO!GHUKVVHzo7>nJMUotH zX+q7><_AI|K*}PEy+RGX1%Cfv@`7*0hyk+|U4hdhT>3UBESkgeV8PM_ z4O$R=Z)B$dKa-;62}|!q{alpK$ZbP_vB$Y5kcp4jI$7MhfC2G+7PYqAtL%rbzS90H zcC)aGI+$YC2=&^vMcGmBu_{okE56_F=RUo!a=Som$Ta!~@_43Al;$SzZ1MVScne(F zksdN13|hfgr=>uqeSA^8NKSEJ7GJ~s6hZ^=(2}fc;|v3~0^Xy`;VgsUT@_H7r|vf0 zkC8MmQC(bK*_#Vm6z{W!e0EX@;6a6>o}v$i2TRDMwKuO0F!d%70rN`hhJ(uwpZp<0 z-hz2Vg&~Pw0sLpbBkdE>meJ<0Jm6Prsp+r0HJOILSTqMpGN-;u7QOgV)A{G&0YXYF`|Y+zAQ;D04)*V$j%#v ze!=&lm!}e-`?_?N9u3m_2oT~wfQZE-f=M8_M$>3x&p4zXt@v!A1bc39 z6JnxCRm0TCo|z45soQ&3mr?>^PiZvhMmPycg04Q3XBFHh5`gVED3i~Kd?_h!r^1>m36!7!k| zPU^Qg4^osjv7Dx#NQqolSwdWMaEYG|Ih9X(_*&sg? zvW@fU99U~UjCu0n&ka19s$(fw38|)KOdhgrYiRd{V&sLP-H_`d&k+N~cUqL}KR@0? z)#5=UfNYraJ*%GrC=jy8B z^x{z&%rSQX560oWq^jiwHJrtk1PSt>Jm*jwl>uYJo2H*FSFJYw&^p41MWn9N7Cc9* zN3X%!V(I4Li3Dz_P+KZ^|A^az81Fx z6+uPZomG9wr#D?E7_GXapSm8p?5fI)Q$Ud0Lh~JI#yy2!-u%3$!nirR?A^uk4Y%^>2@d*{01;)g-fX3vzbg6hdY0;!POO%LwK5ZIjsrm z_p1_#v%b=31KK)LdR5doS`m#iSPhLl#)d(sDDb@|^o`p=N2t2z@M^S;j(=bPuWDhk z5>f!2d*_;bNNwh?g~j{QUA~s2sRC$^di)h3b$nLb1H3TfaG%9bJIv$a;ZDS>zib#P z^~V7^@!a7_*izA5QWo4$RK|#=PQQ`*VbI`U4xlWD8E{{;HzP3K`|81gkjc(0k+YTg ziaU6#xvmS_oIp1(FSOHCc6=ZdQER<$_2hlV!W#x`c8SmDWRqKa&}n!6aM1ZU>M z9c4qB;`*791~7;-Us$5Y1i2-JzAk$R;56-tJ4k)!)J9gbvThKd9jQKgIl;^R#6Nrh z?R+wn3{?>*E^_#_WZLZ?4sJ(-6bV(dTa*^T2QPnk1WY5N4!^yH<%nQiqy7KXYN zzB;YuM+*>g?6sk}(AUwrROHZDAKO1J9cuI;vpJL=rixSpq4GaZ+Go5pSjD$K6lwIv z5+S*{!RXI`6GE7powrd#F_bj`E*U)`l7A5NT`bi&Hkp`c52g#Bj2p7Jrqje72k;-A zjX~SBg7?-HempQc_j)v#-de{TB6O}sQh$2jlqU`?uo%g?sDZ;MEhQsgx2H#h1mD9b zW1>)X+fhP6ve-3nH@lgi>iCyPbfr~_*|PcnP~7%QZGl47k-418u*u=Mm8_TLqwee$ zDqgpxqFHpbvVY_4!#sGN0O|#dvl^y)17u76S$DVu-_afPUZb0J!lV|No!o3w%IYx# zXbTrlWzYdAz3vD}iP?Aae;)haP;01AsL%1fEy7Ja*O}xFXH>!1pAIUkyUYJ$JuE7aMroC4KeDLUVrCcp4~)U0YjPc7eWBOG~#mPH++Rya@PPSWE~-iB7ya){;p0B})dLzR0E?0ACla56Pt z-V*Rq@B(RYv2`($MiSq&a^gRv^;VBfRI#OtbzcvcjcM=LD85CZ?FR)By3jx=++{a5baSxE zZb95&#gJoM3O*Xg{vtf3&&*-zrRaBcyuuDHTpN@--|A76U*Kq*G*dF2L*r@&dBWLx-@Ev6F99H#HW(kLhN4><$fajQ1uDDj54`2kl*8OHO^>SF zQA^B(BO}nRP~WSkR`%!PseAj=K(9)tvbld$byeHlod9eb|@o;&Q7sy>OYUW<0IPl@Aw`}NdqZfsPk z?=z=Zj=!V{K)Sy|nTpq5+XD2WKnDZUtEUM!Y$*zfwBG74Jz92!bHS#;snt_zBGb0vGA?D1?exbEaQO9Ph*X8Zd?jQhvc z2Mcxtu}A~-L#NzRNK=cXqe^xcmK<|fre&c#D2HDUlWaZDQS&u%nUX&!)l~i=RxT=z zzp?AKcV+J55T$+XSiTZ+rC0F?FPQC^1lhaXx|U5dR9n)~pp*2u%LBqqC^BR{8!67f zuWfE(U^Hg#LW|e;!cN&kriQYS*yhvwWXBKLMGi@PkRyM>*Dk26UiMo~g>rYf;*L8Le?cyx)to@m=K+Qnx`((y@^hhL)(O?ke2a-1+7| za2?%IVoW#ExNbM)Vw?dcu~D!VV4*0GPBqyvc;EG{Xdmr}pbKP0@dr0JC z-h`DTHMV6sXENsM^Xr(Sr+s`8GWWYaGjrKq$4VZw;9%7c48PO z_UtG;TtV+Gm`nua>yz>yufc>msBi)*S1i=O@3(%_QM!0~UOiU+p!m^P>{?v)t?|$i zO@ATmo$eijW?kX@+w*tM^wL>+)sQ7Jk)5%N#<0+Z$NpaAMB)d3U@tApsDsW_*t7>O zsCqUCve~L{h^-#2?uiXMU01`m)ln-CHX$@}KRtel*Rz+n|LNjK)H)DwPho8By0U!S zowp|t#FkMXZ^YADue8S{5ij9QWEe$G4j5}#|5IhO1sf6oweNBVrw&u5Avw>dqSc$LRe4id^F{uZ$!X3UvI7aB%d(S=qU)+?O_~63j?2A;wZdD@l<_IZx6(u1Zm`tmHKMe9ZV5b zp!0>j{tPn)MxUrqMs7?>jJCC3(uTo{WtO6@h3A}+7v`_9qYOdLV>1{qj`klZ22Lah zm?S#z)z!DDf!z7VUz`{&!(sDyu7PFKpyC(mMImd@JYhQTB+en3VxwHpb2x0GA$>t1WcErOHz6$z@aYfj zWbB1EOcml~*AUuI=%e9DQjyrX{R8PaY<6nyvzAUyq}$8KnOQcNLS)vg#k z0hA;1+c{x}!!X?>aR{aL@yXDltDB%%C**88vyCp8Ql_UJSWod|SS`F^83Zu&tGXI~ z%sUawlXbw>KaDAq56GlCSk43@*e}}YaMG1E9C>D2X=&%ahC&9tp>kNc zds|L-Nu~xbxDa>ma$;WB_59QuIEzv@YN~y(Zkfniek6pQ-rq8Qw>BhU>wz&w`CgHm z>Jr2wpaJ!@Ks%}A9Fa`k5ks*T&%Kg2mp8o6ecQFpSN)|r`#J3z=4Z zV0mY=Ai-+GYc9NzV{|Q>m;F!>b1l4+l^eT8C3FdvN1A%XhxO?Kdcm@?P9s01BqkT+uEw_j^U{i|1vNuOYtWxL-i4G_)GhhKZ^e zaaS5;BilRkk8;2SC)F`rN29594~yOMy1%J1$b4gW0fYb6k#Bd&kh~z z`6w1My(aqML{q&S%qJ_-b(uylhq)!6h*zpuKnoiE!CAV>{4xB}hY^zU-5Xki&a^cg z5pN#jcz~CF`On|c4|V&!j2Q%V$i{~`51Y_hml|PmMu3SdkKgu(!ty`4+Z(10BHrP9!2m*6#}s+sdz;^8!#%2M=S}q4v$42c$iuIda}y+44*I zZlXaMC06F}-yfnNKdNeb)rNneCiK%O!2w|Pg?Sn?L5bGFpBS#t6?dDi7aP9carh*d zv%qPmoshrLrPnEu-yr+}CXxglh?4Ad1%;8S+sFWz#%?gFN`M1=kv&KygDX@V>i9Tz zOl>)Xl}0|YhUCMgg&OH1K?_x8o;wH=;#8O$@Wg+#WHbfohekf${Lpg?A6~=O$Qr!5 z+LbPn#x1}8$@?sxQc;!(0tR=i8`6qIoyw=~A(N_9@cGD;Tlks9oYf~kywyxxhgo82 z;|*a)T%srB$wML}+5yTDJo2d$;P3W!7Uv=$%~A_g!}4Y;;u7TpW=SxL_TRt}>#3dF z$*Z*#hjjdF@;-xDCQwD4`$l(?e!9p(yPq80q9{Cq%t=7D@MLX2x+Of{A>$?VYmT7p zu+cB1;`)abe5#asLdCU6dgTXkZ~|_D0FGy@HWpRk97O_lwhEMt2L0ettg}s&CKA17 zAyvBKu9!?&w~pL%zja+T`&BSIv!u$z_`wg!U%jvpy$mgEqUIO6KvsGBg&is}ZmYnG zq>&L<9*;yngaqp4V0T_#H2m_EYE(? z;;6(EJ_|ShX5N@c&D(tGtT0QHe|xfeymHm?GP)>RE3Ib%`iMn}uU`1pDBg1go^MkM zY^A_mG!`ri?%vG&TtdHD9N(WM|JO>Y=7Vn4;HILT&oTW?XcLSJCP_Z1lrup0B=l27 z+ugwVDW>_%MtZ;=%7M6Cc_cR$?2f-s`6;UbS%;iJs^ZT_8flz_vLJqf{pp!sWR85= z*ypy;tVL5rL%MLXCkP?nCMuMCLh1_kC#Ihg!Hf0BC_kigv?1Bp9Yl%N9lK-&1<#pM z9754I69_Mt>?pSR0qDQ|GB^i%3Qew~m^-}1uyG=Wyfr_`FyB1VS-F*JZ>EEHskT}| zFhpy|@yRJqv*rAP;sh+cN;PttZ?Q51V^BG@QND7W7A42<#AEOzcKdPxRU*I}buQ2u zMllMRd@6}|a8Z?9cp!b~wpM-vArCt1A{w0C->BiL3d?1p$X;9V6`y~L6jYD=O7P#19 ztNkMSi!u`uko>7OX~)kS!mr!I%ccGgh8hei>fgzv!L%eNie5)JKGoUcz=~t9Z*6nb z#?Sf?E8C3om4in@&R6nz8wV~x-$7muR6=+{%96?Zb#pAlx$9&UiGt!j&5{EbkjU-U zppwn@gK>lu$oSXFkRA1V|hOpNmAek?gILLiw2!a zT9PmEq6lI>V-SQ$bCey#>RRi0^5JiT!66s6$ zs%S}ha|S#IItdq^Ze8xqo0p*{t&bLG3sKbRYY05Te>siz_<>;r@XWyA(MqhBO2f@ zGH={owB+M?hpXlQeaLpaE$bHhRSEOM4AGqXl)*h=Tau@#Z@v>AQEHN!Wv(R+Mm?cIs%r$r8MU=k35m#_6J=gB`;pS@gDCG2s5=qhHv?val_6? zeYDhQxMZb!T45XsJW%2*VCdGhbkLB%^7xp$?nADRGUH9FsMXPkv*lt>MLS!lGRc)A zh9AD6BOhjdkNA26W+M_tX3&$7g9Jq625%V4TVWk~54j#&+>ZlTRUIiuJ z>oQ4#d;$8_SIpF|lvS~NWbEuk+l3r@9<9aDsuFlT#8)-DkN(E5hl|9)v=_O}%tiUV zaXn$qN3A^f)P=yoVpra~e!krN7W{iQM)e3`oqY6U1730ThaagfDn*)+OGqMyHkF32 zV_%&~O1;4HQUPl1tHf$VvRC(UaohH;hA5I2-*T=E$Xt`?{VN0Q)t$5g3<2EdqQa~< zXcOs$!7brnLZlq385KzW+lp`j;*V9+Q&-xPVm=P8_VOp*qfiqDB7NzYRKYNl>4mNY z%JAyrN?aQskV-zwa|@Y~+CIz^(rrDAqNr%XZoC<7(-n;hN&w1RWYs>;V9~S%B+N`h zeLeFd7T{sdzA2x2#yKA}e6dD_OBeJM-YS?n+=%?%9iT^;P4Sp^WW^WmBUSypYspQw zokA@5q`CIo?9Wb6DQ$83MKyM2#EOKGG%2jY^*}(Cy`1+uaqE~C`2=l%p(GT%X|kV- z{p~|qe^5BZ+&5QP{1T7@m~dO$?SwcHY&XpJlI><}S0B;l01X7sj7L>y| zKrC`udJnBAnN{uO%n*0~G^*4Ajq~{R9r-iy-U%jKr?pG%GBPfBSh~a#TFBky7s#2O zjX2^Inh4b!MxQ81kZWKOLiKkMQbjeSn4RY^rY&O;`e~T}J%IkB%6<)bh|JktGEP1Y zuh>F#6$vyC?OG>X$lz50J|VAM-~S;jBLI({Z{G$QZ)i6B_6yTZs`;eaSP?L3>n+Wc zs9v_Tk$5TCiAJEOIjO%3ouBQWfvK!*8cJS@UvEQ zo!e}|hAMlN2x6&I<(<+=c#hK_?a8rIe$5_EuFHTeYK5w1{ooEQAZ`hoU4G$~mg66; zIueNyu;-083yWYe0s1>G0zNGP?ct4j@9C=FD)V7fe~p>r#7;@`!DwyrotOk4_t~#b zM8bFN+V=G0;x)h#<%yO1l?qhjr6c#X7Ec(q?Cw@sD!=BYTj$2eoEkA$voz8(?wU~a zQJ5H<)&Sx6oRN-osioW4?ORsB>1gdKHLiTxe8FCD#d22lbC4K4gwhS<L(SVM120#L?rwC z#Zo;!j-|$7sKp)SSKs8)yO6e1b6Bv1;o}3;G}>wA$8(S;l%vlZ?U0NTV6{^tK%jbm z(nSdKDtF8oXVWJ8BpflkOpfL+pz!g0%RUf_X+!y!iZC%2^tMNPzA${@nPF%PJ*9~{ zgJ>=XJw38NTqjp$^`K_esXi*|a>0D$DT$$Zgx=Fy} z_;BJRTnP(r%3uq~Qy=Wwve=$Q8^NaPULJ?%aD1n3Ew$N|07FgkrN&|bK%Q(D4@ZAF zR@4&Xiho{mtOB$UFXYMvSz-vsKwd-%c7{%YNM541u`!OecZNiot5SQ2I4g(3e#`WM z>8+bbS2Lb&1HY~rE)xBxAc&b4Ag^058h%scvM3Cm7Eluu|IFw z2JT+;$T7PN>&~so9L-$tXvqLQipyUeL+H_9=mUg2f8g2lY*3GrY3o!k*>E0T8Rw}9 z>(`?>6TL?(mJQj6a68mG6+3mhG&zhv(8>h+AT)AA-CsIS2~oi&D+nu$UsjOD%O;W5 za1&P4cm+vDl`QX;ygs+v2`i7Mv{r<)W6#?ua@T{Ng=!CfiOxkIym3vL5IsKr2I|!t zeFx<|ubRbpTS(J?uMQDkkVp>dt_Q4%1T_7-d_Psr+qxF~Nm-NBG|Cgy{}R#m#hGg@ zgeLqtYEi!|%k9gp>szk30(Qi*##xGK!g?2cOvPm$g-*{~9O_BvX=|?Zkk^lFAAqo@ z6WwSL%45qt+Y^Fu&W@ii&5iWGILTk$Zicc4Jeb_0x7YIFRx2y|f(0WiLRk;ZziR*I zCQz?!R2CTT|Gs*^Q}3sqylH)oyA?*8>UYtvfopfx+{=2~oSbLiFEPmpPahsPOBbvM ze1xuRRKj@X3KlMuHn*fB6VXfPCC+e2_|=+~(sDj+$L}YOw-`jS7n&N!_?ADiTrT#0 z4dDPhIImOo7RqPYiJ1mcvz_qN2$`-UPv<72_$!}Nkza0y&HZ)c&mP{8&M3{YMz6Sp zq3`g}sP&Fr0sfdcUfgUn{N3d=D`? z6*3z4Tb*$DxxOovB7}8VBqpu(SJU+1ZydXaD`RaDy`URQMyQdAgGm}!;X>_M-Vgtu zPu7WL-3-m!#gONbY;S*uuTo%Y2s6{b*IaWy|?qh={DXnp7d0t zcX#CQCpRRB)XRD7!d~zBMf{ZO^HbVy1p*Nhg^%B;cOu8Ytz;TPU+~bLhxz<>Pwcrc zXn)`B9|i5y(8w?}iIN7$isL2os8UVHN+xM@(zVJd74HiIZMy#Z&C?e%v5x^>I+r{n zYA^>$Kn!$;=zVfsRDA!cSo2 zP{PMU8qs(>gP%^?g9V&spP;W+Lm&zG39Wpw*I3`f3%{&cy;XmEm`7>Sx2Br_f5XEb zqb?F{c{A9IEA)RIyylZ-wGa~tWH64k*0E9zNO)dPIW^c%q& zCo-Krr$@7OJ<~}h8R5i40tS(^J)NGo?DZOM=0Rz?z+*Y&h!j@e6U&}uI1wo zs3Kwb%exVEntA;C``TI_Z>ACyLA&&&RhY?FGoeMGC&z87Q}13YEDY!U z5`gYgp7V$kgVvP9Ir6oEgbwclk_UnrsuMh`J>+?TIdu?7As~LG5yBX^`TQrZ#*QTS z8-P;b-eQ${K8Te4322?Ps77$b_$q(rYR*N!R1af^AqytholldA07BaNqw0dLL>K5q zgUG{i#}MG&CB0W8O8`a>eQ3JdMpUzE8k#hjsCYGERpn=BYBiD_`r&9iRSPYK6bo05 zDGZ04UI`|?=&q(j$YB|?&D&L&eI};Ea{7a`yfbj`-bkFjcrf#@ED(zLE{^XZ!H(bbV?Mk)s$Y`k0O zcQYuFHm6hNr>qI9}UBmh||+=^J|5i1?g~BQ%`Naf=6&8)Aq+Boob^WX0aBY^vL=9 zlmjAI@g}1hs_}dXoqYB8h>9A(0y@dv(Gy{tZ>!a_KE4BD zBPkIjXf1d|I5iN5l@%8TAv;P!->ahnnezoTJu%Y8lmWEzc!iJDZ%KQ-tAYm(*YsLG z)3(k8_!9@vBxoA69{P@b^<9EV2Phf>8Z~VJkEP_H7e-Oqr`?k2XzuYK)LyMAVMe5e zP_tPFM{F@)xu{0xAcOtYdl7-LHyd==uQ6?)ybn9K-rg0QS|TPwiaunIB5w(tkq{vigTJ>00~uy)z0gLCjGO68sL zp0+K$N64-g;Kk7BHpxem(YSu1_B)k=sIUb62D(9|nDBGQjc*9Q@QpeRh#FVP=pmQ) zL@k;2p#|-4fsnYU@McFG)6;jiU9fOnp^eO%VR~0O=3E6 z1?gv&m>)EGxhr8ZwKAQ$GBla-2xoV*SrZ@*zKfcG(eFnN7+!2+3fX>LjXsA)X%)Mt zINYdyz=-ij$AJQyT4%?9!T>5ZQ81JG{L=LErj77 z`*1<9=DddaGq**a^XSH@E0u}V#dcW%AkW?5G9q+Bf=Y(3b?u@VlJ}>kkF&BQWPVgK z$qM2-5uLJbU%4PZWHE=@r^ADwse7T`$82ifI?EWr+SrE!!G$F(s5FM!Q(#G`cvI~Z zBuegyiRGj}yn!n%3eoJC9(+*tXghxSK+}j0zZnTbvn31;V#&<09os=LS6**~Cc)G;z@s8|Kdoxu0G< z#ak_+|9t~}dPKQW$2=jzEw=a zAWj&9+k5wP1B9*c&89l9afmYnzgq%rvhEDbys{K$Rn|0`)s;zLXrtl$ z4$ATe?J|XQQji_>6EJTMAid!7;Q1&amgAUjCSV8vV5ImBKAuQERP#`OghRjPedU{` zb&l1+K~5XaT%N@p8^WjlF_tjeg8gsFpPf{)ul=lsN;M37uJ;_}_V7LYVdE>!ak%$X{f&Hunks`8MHVeH(n=u;=HsWW(CR0NX@(u4BomwF;ib_JqdOa)) z+o-l(#07t6AB~*LV(wy+^QyB~)J#9R|9W*VVDOM>Pigm&%W{M4c=z}!f?P_P0saCwC|}x;(R*8H+NFyxCE*Ul8@Cx51p?M zJzG38i7F!*8tQb)k|Kx96p$3R<6ZYGic)`P7)r+rVq3|-5a9)h!uQ| zprV8&TxPxy@MOV%=w&nArq3MhNXt(On~<(YeZ%}@?S`RtMM5!y5gs#$LEb&3wdl=> z<`VKIL}siHN!O#uGXHmd+vQH%;$m`XQNVZ}!?(MH%4E{OG1R=ch-=x3q5&!E_IhX+ z^$X+I+sB6C_JCS zoZffT^uSP3R1^))vk#QoZ4Et9mXH$k1=RdG)23Mj37CB9QRxOnjlMgB#b-^u(?Uz5 zSe}~L^w1-z^QdO!{JCfv-CgQ+%N_2F1 zZ}7uz9Kp%BBI(C%X*3BRojN~HWcWS?H|tZ=JH$O&KE3J|zQz{tJ&MW5`1lTZ>G3%6 z8JK>Gs*7#v{$Z`4h5`j9_TeCtkv79^w=*v5dWb?16Xtt3CGfCBysiFZeTAz1TPHRZ zWU~`0cC|2NE;%p-hQ2{{PMC?DAXCK+G3(cV;TI?QRZjbZy~f z6xr1@#1`p#+SBfb9aig*|@YJ7p+8U(Y^Le5bx z@w<<@mu1Gc>~gz%?h9``qUZ1*$XI4Hb-2%U)CCqnkHKdG1wm6FiI!y-mX;%EUlG;I zuI^&Fvu)lbW`sxx7+hmVf}5FDfZbB*vNjbrdBHaUmf`pd9(!9*Yz?`|pv4`4-eQY? z|Lw$2(`b~cRTbN|WuL1HTM$bJm57Ok9Jq_#w13syX%Bdq7reIRQjIiXAKoCt2*MkNHLDXgL@oT3{wSc@0oC zJlhuMQGB&DN>*MF1S|L?R5!g2Hjh4DInjpa~Z5CbB69& z8DLG5UIJKrw*WvsN{iWM@45+o;XqJU=6$##lhq1q$-iTPajn{hp$SI(dU$M*KDd1T zQdWeDCGRK&IfTY*$CypGDzni^cg*jotUZq1ae8H0<|8RKtgnpaJjr)$*dz2tFZj}3 z;m4!Pl9>$_TBJaGT$$lUm*l z1#%yv4!LFfTH4&bnQKDAILSMNsU@zbcs8}q4Cq9FW~y^xY*b}l){5dRC9o#S{tIY? z;F`g^sY9|-dgMpW@b-yyrbyg1M!$O?gOw=0uIrfjaVQdef#3lZTWjDxzTKTT@bD^h zIpRcV(n{`Pb7Q_I8uVF~`i|9*GGi@@fJBbrUyoySR7v|Onq)YJ=s^?f?nTR#eQf88 zx1l+qtf4piwN=Dc!v2bm9w&MCMd!Smd_mt<`_*jvM%K5u+;`_YPp_KZy2oo7_l4p; z5R`7sG(_r5f}m&$^_kT=F&Z+Pdl>cL%EfSHIUlZ8Q5iNh7oc*)uGtfv`4oIhh&osn z+*_!JSd&K-S@z#7tIwtUNjC8`Hpdi##rrMLDaiabZ#^e^1%Tf8s)(UqU;6z8KJ{33 zElDtYV9H(maCo?_ZGYQfIJ`n#C;j>{)D`mxP=|HzFCT6)P~h!04dgxD+isUhHWlKf zA6!=S5j|$T7%A=C)2bd;-fIgoay^ZIe|J&kUdmEHqddrqYW9~3B|)gDc2B8x{qj%n zmBV;+7AQkPvN9A{>z54l6CaxF=Tj>q48_e`h8kJ0UWDk)x3zSQQMlb@ImN z1q!e{@Y3%Va@x*LrKq+~cZ`CaNt5nS|CFY3tMH9k<#)TCcO=Q{!Gom-o`qv>V<(C! zL5jTKs|48v>E8rIC-yTR>#}!&DC_e=5NT~7`y*_)GQ|#Wuy{f!UKgGl78yorLcm<@d1yB94XPL!xF1JX znV+@dA=wI?O`px_1Qf*Im6hZ2S%=wbweNa{D|XW@%bXM?cDHziv10kQx|F!x;xFI$ z@UeHcmFIh?c-Lzf(S4sXf5V}aFq2vd1I;2kQQnMqd z1&U|r1NIT@HCI8%W=(lF$Qg^hV$+8QnSzTD{D~5o zGtY}1>k9=#09EzIjec{Nmb;^>1j)ZZ0On0vy=xH}(`q1Gf_i-I<2%DO$<1(#DvFJK z2Y9xB>WPx_wNfek>fQqGF@Yv^hg^;9)%kecp;tptpUacR=^N=7b?$kSt?OuMu^W$U zAfs>df%bch4wla0rRwJ=RG6C#V#k^y?;Q6y@?~@GOHPuD3b>JD71WvQ?n3skX`L!#h@)ow0ywNPK4|sJ=d2F7FaZC5Xu9gCsJgE`!_eK`Ae{oz-6)N8 zcS(bkFhh4K(kUg~9iwz3DLn!Lg3_JeeSd3x|FPCA?mhRM9naqT+55O=G4gKxKNkS0 z7XR1dtLdrg=DT&E`AzxijthiOKi`E#8A33pOAdFYXd3+C5<1>_1k)WL`GL}w-7FZ4S?LGw8xs8>FJvz2sG!%yKVge3v}pR z?4vsnZ@YAh>tsj#&kC&+Iq@hDc5n{v5A-2tg1Z6ZlTv;9%c5uyHYDQK#(hxTM97U{ zYy}+CGCBz~L_`v1{K7PvW9XA8WVocTK-1;NdM%l1>Pzz>g`PUe$yt0p_WT@N1=|oZ)}gVD880bKpblbPbz03=c?cGysH7 z1us5N4dAS7lTYjGBAKv2aB6~(`Q7EL0)u90sycECLI#?fa>uCQMuYRWZJ@toLXXc+ z2^kK|wl{O9nc|LbIKE>P+aJb5e<4YZ*LZ6AV#!d1df(hC!vz?-F{=sXa_Y<_L_JK9 zb-NP8DSl}d+h_vl8ifBUKD!Jnw6$!DJq%C{)}krfT6y2=!l1b(0F}Yowy6Ju+?>r` zTi4n}e+5;IUMxo^NrnP_sG*d@A=E6|$TALsL(lgEyq7iv*|gV+h$7%)NIVaeCxEW* z3%P!-Y1DEg%RcmenDB~(c#|Aa{}88AKJ#}Ugxu+y_W*9jgl*=Zo?%PN(SNYnD4ZZh zC@{>US!Gsi+l$keJ{-r}d>~yQ0f7dAT9pKAggNr(xRXJl*DPpbjQ{n*8^i^EdVeVk zq9(pOS-M?)5rtAiSZaCVd6)^%3DoS|KRP}xT;{!YT|l_n z2Utvl!LNAHu|edPz}6Ev8+Rw&s6XXs@vd4x`l?hv_6;i`)O#hB+Y5P4ARLBA-rFQh zx-b{*(xc&CGh%flf#K-Z4>kai)*e2%e|f8E$#u{~aD7kqPu`>%`&wH2IWNB8C})AI zwx?3Udq!34Kz{W_$97#$TmMa5h=cD;dez}*AGzd`I?yWN2<&%3p~hgJ+@b8i;R98X z&DuXeY&1L%`mMIa{!5W-DCY^)zK|-*dfX4$Er42-k&t$0x@v@w*is!3{2R_k{Pr(< zz?>my#|#|{!#qenxuM{lRvB2^+Whq&9dZwE>Ti_3+XJWAX@Qcp!VtAO=1LpCqlJ6k zf^gM6K#q(>K;QD%NaeRf644D~HPULGBF$H6IB%Yae(X~(1;=h2t_C}b7ySo6e=D6? z$dcuTDxp|NR5rYlCF>U619}nxLM$GUFc=~FU{}ns1oS9vH{f3W?So{d>Kkg=9QFYr z3~>>pfR4eFRT~C#og-m7Ec|K|v^9;~hJzDS*(~fQ;qT&MPnoTA#WN49AR}om8ORcI zv>f#xCLCj}-B2woH3?)dU#BR{ zK50d%E6T!yS5%?jO_GK)DwIxZ4JGx9=D%xo|@;l7HM7(F^`_tiZw?Nv1irs9XeaYU{=vGK{ zhVcz&GxeGK59+4|+(fR^0tz4{5F0SGnu!UjtDhEiTqxmdh?m0`%o+vy0a!BtQDipN zFZ18RD6)fF(`DE7pPmdOA%j^{Vi10?331XzV)UE_C#2hml-#(0` zs{feD-MC>we-F9#33Q60yYHL=XsPF1g1bTVv2QQNNG^YP2Q}AgE-s%QO`MM0`ER|N z$J`!P>tOEMd=ctiFt@XBvEd!4AwEyJo6Wu7WA-bTS_osBvYw&`&3ne<#PA(PNul@F zKfN{8`DK`m9AHmKvNuE78U$RFv>lT9;bG0+kpw=_piNVF5IA#tZb+PLzci(4rZdvuEHe%H2eJX@uF zp+Go2HO%_mGzx>kYjC=6_JfZqRv|C>P=S&r+=G}gMiO7LL^}3t=H-5T z)brA4t!wIiRPxfe(pF%fP;h#~S)xlO?iN5F;pn5x*L1>8-_xJ-^y&Vw{yAo5_KArH zwG34Al`=jw4Bz_1ws-A=PkBbyy#PO-@A^&6i1)aHgh<4vo~z)-cU-4W?)`86Z0^DD zojNGo96Hdlo7WPyfxko1j=;S`dEL-#6&^X0a*9vdYbPK<83nR8f-7tqQuVmxpMbSR zVGUPR2ZMs6p0Ijfc|0eX&{iZ#?7JzEN)x$)^Ir+J;;zuwl#1)LkUk?b?DRb2crQV5 z^X1fR)n`!4H=akatcCEe`;H#)BG<<09BJFeQavD=XI0J>p>_$aImEUz8}eiH=Zkr{aN=GX%l;tjPL6Ee z+IfP$>qGyU0Y^A^Sou+TUVf`P92Q8cIgB?iKQ#z>K6&`{_fHZ0Nm9 zWc%};3vHWP+UJ}G90JGe@Br>e{6@bhb=rYUFn+Rv3&Z5=H{;Z_3U>j|$Wt%N6+*p276U?Hx&cq`N6A|Rp0a`yC!?n%&bboYEG8t1Ab}ios?rR$ev$R*l?3*L!j^YVO?xuI z2Fj}~jjecGKcs0yzlOs&YJ!Rq2R$mnoZU600+yS@!0Su(@2$znTmxQ5YuKzH7u`=8 zxUmHqco_YLT19!WupbrAfb7O8P{h8(q?R2Ij-#Rz|Mh36-1@PTWsljzP#@D2iCBN;78ENzi*`-eAM0;8j*0qlHh$J&J&wl zigp8t34s)v7gW+*vjmyaTj%JfS70a8-k4rY1>;|(TK+y(O4epAdw3D6SbZ?3YjoJFMV`gg(W)&hi>CryPq#jx=MKdJafUb+-Q zIlav&%U8_$Miuc9YGTiagrNc8p<23vP?(<_;xeN6TZ>MH!2n3 z)urnxEpIuEaX_r+&$ttpyw8}KnBBkG7LBmyfL%gTs9c=>K)i_LzO}jj+yaEZn#?jj z3R79ElC#rr+8two%lf5n42XcL`(s?dI>bi&akWi+`E41V*U5?G69fumYS17`>x(sh z-uvj?xb8e-NuW=kH}F7?$_6rr=(l~?rjV98`96}g$nun)PZ={j@SX&0vy{v!(+z4m zb?~Z|^DC+9&R2QM$Sur-E0OF&Aec&QNMvLZbV#k&2KHdGZ>}csntOx@)HrjO|EdP$ zlp*^vBp6X67oicHaoZOxqxM1J&kP)TGKzqH>~|=DK;I!p~IW zPIq%e0%v8bu=009eY_*zr3t%~@t2-;>R7!n4S zjKT*;vQ4afC_s69a~BR|d>iog&u0P{$2}lVg{=$u>_}QtN7YKS^_K z^ar#|f_#C&x-EqTnEC$kkv_@S_z1EOSt_@PD9_u zt|di+8D{fN(ob^PLqdMO9qxSgouw%V1c7MR_;~32Q954roFH}MalL;z!~JAUu|+eh zO2dq;^=ktph7Gmf-+YAZ{G15gOw;P|T15%PXYVoIKJy$`Gb`*_`dFdRJpBCYlMvi= zZ5l-+X)Ru#W+7UH1YAc+-ibl^{Rnu1_snL;YX*u>q@iBa%f9VjBg&vP@c4J;1zXCB z-;0~SC(!G^EoIoUyWqxqM{WS>jzK?dHzKIV6cE}Y{V<{QEcAoCfE?uT#vh8$t>dy$(nZnt!%<~Umlt|MpMKkG6#dKNO=Ln(mp{(l0nQ`xP>@}4{2y4 z?PE4TjZh+?QHTc5fW8JSjO$v0N-SLK?Q2PnBL2keCNzC>%Nw#r4DYKTi*HJ z;8Unr(tk!1yhMZ`c9pAL2qsIqq^w_Nc}786cQKX}M7RCJ2XZd5IP%HaJ>|0h8SbOF z>U<+|4?6`fKZc_&<^B#r-pYOQ-)=`z=$`O-KW18T;ZgT%+wh{WE7h1=P;cYE|1a!Q z7#=f1g$~V?lQMq(mlK6|iT&**=3{;XtwNZOMjJyB=wmlVu=D$8m{rE^Y2_DLD*0%z z7R@6n)I?SAbKE%z<9OHR%nb_M!np5f+^=ubrgQ|fiYjyW^)f8+CYS^#v;saj`T}cQ&y6-HONo2X$Z9DpVf;YWW3DOzZe_Vwf1Q zw_k5#!1$18#B7d4_31_hzS=nk$A|d1r?pgf0oxaRVbaU&a8 z|F6>bb*M3tkH+&xh^snc2^CKp(y>?lQ_o{2kF6j%N<`z3$LU|8c@!;upLe0v>AEnjd#uT1iV$3!4F4CcmA6)YGd4eT237wO&~`seM4YT}9v2pt|(`ZG4crcmnDp z8t``T#i4~oaezd;{T1$qLev=ya>VyvY0z<)i>b(G+}^7|r1X@++&P!jqsUI_da6DuVdFeQz_BMsRnyGLkZ|u~%RE8{!LI zwfRfJ)J90_SUp&teptE6Taq9B=6i7@dtZIQ|L2z>6M}KFe<-!3O>UnLbX90^QB3Pm zy35a<{Zsq`<}4#_nGzpe1HND(H;0<8X@^w-=^YRR)bT3FX5-P~Q%=gpiJLz)*Y7WQ zcU2`Xzo(+ZqB=GBL1p4_Fw1IDdXIFhVBl`PNk{Fkql zosalh(i-D?3=fn<>zu9)G%1)I05bB%Gw>StXwQR3#!AM zhbSR5K6|Uq%QaTlo$ue2GT<2=TrzWDV2WmhT!BoZO+T71?65Li+iH#3Rv`l?Kw~M= z`1#Kcm8Gt+n^I&tzpv!L5|gJ`UPQ`EfkTuNvT%Y+X#^brHOp|r^g@3yg$e@Y8d4u#&5 z*WmL)EfV|l;4bExjPMDiP_G8kmps%|sI|jg6VJN*%#zeD+rTq9fM>o#l#gVWic^0~ zu5RG;#i^JCsZ5PdL3WFaUNs;efQ6By6O@b1hYPBx@7M*j20rQe%+k0e7umQ<7Evwd!ZtEO(*K8fN{aN$uc=E2&ipH(sP6JWB-AKYHW8<0pN+n5;_oC}bc5yxcz$7NVxX#wBdEv|ueZ!JMWM`(X zo=^VF2g-+Cv#0+6_7i>B{_}nM&7LG5(@u|17qge$4_ZhnO;n5S$2-bw~~$NJ@5% zcx8)t&=GqLYt7CCwd*^Hj6-+bqcjl&T6J1p)JTyT`3_%?vR7$pVeh}7=74d@Fcc7d zmP)3_y}FY?#`L@kkH4-69$WR7cEq&GXI6{*w1!>al-!SI=(fcc6!mO1upjUge`d;v zrxV!D>gpGv6b*gZ7}iC!kO;gUP|wDsMBHEJ&2O7&t>z!2U1>?zT2?H&o=<6j?+ARR zV_3@GbV9~f#~XggY&T&CY!oGZ(dQo{*|u>sGWy1DQCDkXx}!cWunfMB*qI1M1c9ZN z1Yry1Py77{$J~4VR&&B00^wXblP_*ya}=p_f7*~;fgNh6L~rL}9H235y>otWgCeRu>*JZcHPoNZ%4kN@$i-)6!P@eSH)eUeFvKt zD<}=v4=xGX;h5ZJX~&NpzCAMD@1ggWK0!*ucjQoJiJ?}W8pV2Fli$91IQmIO9U--| z^70!85dE8-{Y2l-p-mE6l5k6Z0eoL9dhsKzQP3MVLE;mK^{i(_elyKyj@|SbIhEm3 zX(l;YeI^;a574Xqy2oUQ(TJR*b$Ld_-}x0Pauc*181_J^Llx2vb#PoRku=K_%=C3X z|9LkTC?73fw$D;e$o9y<8OUmE&EIWeJYZlXXJ5UXlB+yOu<~hmGc}q7>HC%-W~nVF zV*-pKR0hmy-CCdQHq>9|?n~d`SGmUSfJ`pJ!@uNP_H7;hF+xQ{%0fs@u z@r>T>@8>1(bGUd{-foP`t|}F71-=>JC%faeW8mYsmXg-%*<*2iLEDM_L6nqTa;B~H z=j11hI^5gJ8Eq!P@qdFY9)4BE_PX8(6m4hbt7K(hoIx{E9d`UIaqr~iTT+8M;&(l( zu#-+N$PmdRls1%+)mevc=tkQA(}54vH)Mpk9^rQ}jB~Gn97ycz;cC|l6fsKjYxIqWf<>&1Ebktv5F*fvz+=IMQZ;*%Qc8;|9%i7zTY5A5I>@dZ{%49AbL;s)v$Z$~Y{ zFw!2bLo=(wjL^BBmUk2SrSB!=!MY%#y)DL_{MJvYfN{jIt{#TkX5i*Px=VD=pY_^F zcsIih1#`^A%+*Vz&Rc-oMeHONn9y zCcP+)wlPZ``()zD`_-T51kJRP7`RCSIDGw>@oWAyPFQ8seElBi{V>yyz*oYBdKYqy zCce6$Hyn52pPR~d8M+DsGC3pf{zXwmAJ*E=4E@FuNM=xt=e4oF-V;tjdPdbbIyv2v zJ}u@?gl<~bBKy(SBg%6G;-cX!mMo&JoZo)%og`d%aP7?BY{FEIufFxLI0mKWc3c8x zww4*at5pCi^t&iiS3xmpC4lg&Fwe{^hSaL3H_NHXZyrMqp@IYTI2Aaw<9f5$G|4_w#_}? z?okB5Z6U)ov1_Zny?EWmbEVCIbi;+BW#w zRPlx@0SStw#nWU6pmURAO#&Y?ijlkh9?66ILg1rlhLP>!pSRu4bkRF7Lhv3a+sCd5 zL}YahoS?V=&jmo}AgCcWeH+2M^SX=OZGrFNyT|NaM9*WA@l11v(H#aP(pwilRP0*LER7a{D zdKHriwkR591r3eYRZC^A%8kY9qvowh#fR?XZpEFloCZ8&61c%o=1D1(V1@c_uyU%X zbfUk2RQvj#cqkOuHmA7uCld;>0e#CM%~#2*=5d>OaTJLOoT))QV_VF3EvQQLl_5+X zn`|61i@kkFSaV)|bhuuS>9OtH61eDyyfuxzINE20VxYl42Hs!uwxtO0k~I*Yp*IFq z$poBjg5B|g6a1-kI#kQ+IOgxerCY#pvkuy22Tg3=oP4y)MYarXHf_fr8?`_?j9XHD zNJBVg{GNw1Bt(xA4j>i_L6!kk-qhGVxYf=|zQx$4Mbw&jX9xQJU%}?DULwc$q&r#9 zR3$dWs|=}fwv+dqCs8@7p@XN*Qc^(Sg^!O&6?Y{<7_Y4f)|I+4Eh`*V@5)Y%=@1D`u z!i9@c5M*|@C!I~YurEY8+T_I`W;~QYyPeQST6Htw=hrG$`p8SGOEidYD{R;gXTW9L zE9dIrJxfW0|JxT~kG5IujuG!lWPKEhxz+GY1*j&aTB!J7dnQ6cdHSkN`a56W)x)_-~FqgE$XyfR`3tC-L5<#)JC zviH0#hik^eTTX!POD|mYzvx7GkE+RTlm^cK!Yy{}D*%M}dWRZsPNLj9HPVCRP8|OZ zRe8pNZ+5IhI1TZ)wBZ4TkTU08=cuOBX(Sl*pWkGojh&JivtXfDf|5m~VEsldfeis5 zQVhc|E%)QQO3%a$L2#{dC^Z+J>H+68#GYpB;0b4|abA`13{H2pjMFdOwO`|a#M z5cCn4$wGETb4xm(`{ecJvs%&hMzF)XPnRNgWt%RDr{ZR-dv(ARjflm zkbqD!f%#i4@9-wePS~-0ou>g#J8o)6jSs3!pol)C9f>@ae1zL`T11M(0?7t;3pkfv zf^TPLFZBSef~_S?gUS3QXi{eL{;3#EDIj#q;}}jvTAm+MB@&3RqP(8IXx#RgzN#Ft z>5J$y!dwfTO881OF3`s7M(fv_b|U*8rKs43O!Dge9=X`>+}AhDfhc|L!1vgvJA{PT z$QGn7doaFx1aU%zFs}+lDiAYx zpVcdguVr=zZ9)nWq1rwVQ!VlsOPNS}Iuf#@ecoF^rIs(^4(XN!-P{9qGiZ~1Ww2w$ z$Ih?If8lRonC!eo?R1;%Z(uGYD&7)7H@Z=e5lv68f%m|bojNu@hFlSE`hy6W_iXnD znSPXx)Lz&XMi1*&M*gi;q8FZsKUMLeK8gI|gl~Jmw6`2V`aNd^x4|@5IB0C+oiJVy zk}9L1RZkn5+vDH6Uj?QYR{|#33CA3dz|aH`E(mS+nxX@eYM39jw!M%t-7cE;ut0wM zmk9Nnr)^sY+8@)~lV4(9E&l9eADj(Dh zQb_A{T&J+|674g94^oE(DOHbLU=jik_20~5n?G*k0&3soOfH>Us7sivFb!^mQ2~`8 z{$8cArlA*mw#Qe2FMq$Fjm=MH_VDGn^TwftKC01HV@+jg-`#|&x^@Np6vsrRJOLFV zOo*lRPqqf#rHSTmT1u&H0jUyeG<-0V?LRgOMQ%L zFlza4`{1Gg(^53SXv-o{=756&+?OK_U1jiJ&f*~N{W40TL`rWOz z0*nC%hp*&Dc|#GOim`O!e9dB)P)J^ZlE6K58zM&^&>gMy8J%+$Ga3Zs8ns1Q*LiHudKYnzzVKN!HGuy{M`;%{axCxT7NuA&Et&Bz#80 zTgUYffyqX>7`7z3t)240AAj9rB3z!Z3~@@f>jwKkMqfg39u|+|rIF#F|FQ~Cq<>yK zqw9q1F|n9zGmW&uiEI9|SV z-)2{@hzD$eo_u9cl#K@X2&<$jmlVh}<1%37$G-X&4n;*BT95uE3V^zqdlU#Xd zmA4*3w$KTWLy0qiQngia)<-`iD-axG0sdzMBR`Uv>vq(RN{(ktfha>7Nf-dRxFIeg zJ2B4}N@-NS#VPizopgGu1f!0+-zCWyjXD!?65UVn5>ux;zZ9J&Gc7qgT|AA;@zg7v zXVRzZ_fRi&x&smKffxyX8{-OJ9V3+tZ)BaC9D~abF58f~9{p=htllal5gLu#bu>WD zSnCPei8`Q<7gBOD%9iAWO(sa)B*_XHOx_y>xgY;UyUvK?Bm{s6Dt>G|Y0d;*eR`+~ zkM0R;J`h~^am<6r)2ySD>Y7`fzNyq3E{=8(OZ-~2bJqc6A^`$(i2t_8`q3Q?JX+P1q=u_LLwZD5`)&xj z7_~34d4Wub;h3M3q>ybBiJdGB8@U*yjs6jbcVRsvLX|x)*|>$b{R2f>NpFY=I)04$ zj!6DiRn!Fz4u^fa3z^^#CE}s4PNIBJB*EuzE@t=V&p8AmI%Dt%P~7$oN_PbFrn-`; zTL>lwaVf3W+!Io)!!y{Qk2dxy7RXVN^DHqqM~ks5SX=%kd=e$9u_*AO<}b9jKJq?7 zdvN^c+YfOfH_>=4gis!_|AvgI{mWaTTy;y0a=F_S3{VO2m9#uu=k;{=i2 z&$9W6eF|HTA!ZLGTZyA`X^&4N^MiCq2VfOM#Q%1~B!%V*5;H!c$zNQ|%+S-m$~Y?Q z0wn>C7i0FxJO0m0Io7Ap0$9;|#>QqA0gG$UG!q^=5&xRAisY&3KxdmCZ6Jdje;MKP zBGXdY=wYa^SMoo(P~pCNU{T z$oqIc*$s`h_YudYDiR34$7hpC+g)T(ny|L)(0!`N19X~_UqZv^N?(Hpio!OVJ&}2r zdZkKc1xQIEG*jn+vZ7QLt+3?oSeTyp&x3}(Wm*7NxxHFB|0QuZ5;li<9bS14uG&qn z0u9tgtviqcs5&_`w?#DBJOQDnCaY(mHvnN$opXRdmZ0(HwRf+d!dY9GPCye`CejXw z^MU1$i8<0gFk9SQg@cm>;d;W~jUoDm_kGc#l?# zB_mGmR~I<2idSh(-Z+h`?s&~gY2@LJw_FNpc&zjvi2MR2=xX#RA@s7qTWSGpf@PH8 z239KSTbl&9E-Zf&42M6YdX=^S2Pfr4;5L^cBGMcdYC$NTBK8}CI`o7K)ZmI+v1zX; z(gK1o?2X$OkwRLpIzI*EZH}&3S_a3ze)A`H1w5$9&+6Ai)j=v9`XFoK-pY^zU4r7I+bvmqDm1jR8;bR1g_xx>e>8!K z}K)44D;ih}Sr;A&2yL#j#`q827l-&6CKWpf9=-G<;&bhXo zkR;9zl4r0dWOjkmAj3komXw4?Zzx}y1o?YHGza=28K>=}B_=b3Pg?X2Z*W<_nm_|d zz;h1)1Kqlw{M6+;m9=R?2{-w99`(VjC59!0zWjDUoMxVlMeK$~)@2Gxu$ohdDB#xo zLPF7XI+sApwhI#Lo44eVdM_mtBP_0wOO@pUzBbcBw?yt9xbc^le%xrPHC7Xg)In4h z@30Z;a{;JZsrx<(8Hi@QftP`xR-`@`i4O!L!cvBcMgC^)_E%?byBX8n@E#bg17=^!OwaY-tnpF*;u zH&9KJnk6*lNU!uqtfi4h)o0f~|K!9>J&0a{Ndxeu`qW>lR@aEM_hl+T4x^aG-tik) zXd(;srQqVdOr^S{Q~B!*o-L^$vu-%_d!|$T&Z#sIsy3>_tU9ds==*X@t@r99jQ#Db zB`)EznU4I3BpD$K3D*htPm%(aoq041Y?=KdM? z&##|+h`t{tI9lk`a``0b+{)GX%WTzXzyM=$A1W zn!~aNUo$NIJU8ck!hM|@q_rnh5aAEznA@^|4&0D2(EgAGu;FvN)&j{tE@Kkw;K>G>hcmEEb>^v zicDNrrN$6$)>~4;c|gJ}{MA;bvb@L%_>JO4kJZT(a=)e*opS#!Cpla8;TH@6d%aUn zs-b&7FvC9<@$qXfdIuRd>!|$BLOIj)Nh+MV>n5~-qX6O|24qYK(oo~Fp)L~5t5Z2_$YP&)tC z*Un~$5QG8d)cun~^$Rr1WF3iDevdb?s2g$}9(vbz^pj32{^C>?9@rQ^f}w2Xy;!b* z1FYZZ)V0Kc>L4mGTV)g+?ig!#r)1vo?%BoZP=fB$tx2t&+;!_o24A79=1gTCEProz znmK8LVPLw+i>g}UW@Cd;xb)&Iz^}U|$6uy;Te82H_Ep7U=bv~+HZ9E+lbl2a?hjLE z*BP#*b8lXr{sTZeXNKvbMfmO15>^UFdQ*3H2A)WqzXK`5swkWA9ikw1JTHi7+{wC^P2_$lakV~1%j z>zNOrenh>_8_?72yZjo=leE3gl2*MF0fn_Hx6uO6NF1OZDn49I)i*y7#1{LEGo1FR zBD7VBSjL2jl9oHq0BdKH^EXS$Cu*e&@?9_;l1oiapQ|CHFc+{rwk6>f1`02)P&r~r z%#|R8H^(~=v`_QPN4jy6aE%xFVUubN7@l+gocUcU36|E%Wc#bwZ&2b`u_-{Ygd-Mg znLj?5f1Ywn{H$|kx-`GS7RBL|97LOYAW&NmK6#B81=x607`~FruRcc1>CvuW&+2T2K!op_H&W^$P`<_E= zPk<7H=@fRsSKr!&gVH{BZl)`b*;#m#la^wN4zL;N*xKzTI!}%LK5Mp7ndq_`gRB%Q zXlC@kp|EAMFUv4r1pxow74 zo6m98J~c3=2}G@1+?l!YA^#r`nzI!oI&|Y6`fjp2!hW;{-E>pINc#B)J3|r zD9n#-h4YlO73I5{=PNA(f=aA;U9AkpTaLj!-9gcq_FD@@>w61u&^zk_+G=ni1*=s( z>FjeSq?px%ldvCa;MQ*xmp;vlDF@u{S_YX)G<~ISF2xr-Z8m;K5PPr)b_4^nl?n z0nn0lLuvsA!7B`n_TyFdK@=JB7o!2(#Pf36uT}Fx)Ct1ltFk(ApW-*rt3i1&1Kg42E>py&r4yfrN6e-uRc{fi z)Q^fnrCl_sc81l;Au|7z=r9jd8HAM;!a6Blf?=ID566 z1^@e@aNc*jk9O1Z4SGd6@jH5AKhkSlnIMO6?j-|e7V7MO63P`m2z)IWeAsER!GT&+ zgFi#2ncSY-p*C+z1q#RvA#g?N@Ir_Oo=#h^l+*)uFfe3mmmzSgisP)%0MoDNMo#t2 z3W7j=YaN?O3IUbgh^H*t^s1u^j$)bVx^SwXg_v&nYX5ckEeIm!Ghx31ld&zY3uolr z$2Q0*4zq8zxZr?x-W|!1K9a$N7JH6qdrg8XF@XXP%M9HqCN|Om1CQMFy))W8=;kTB zS1&&N6@;Fyy>$4YTDYEkJF@nxFh{CyjoS?9X6l%@k4Gbj#u$QlLrh=VADwd6vj^?a z&)B4@1|uMVe-Tx|n#zH`PPM1L^blL^NE8Cun*a8<$brJt0u#vC6dANC&`Cg%YT6*; zEAtd1)q;N{+nei3(Vuc}oblDX*8ZrfF^^YqRwP^Jr$uzW3?5npon%VbeoJQRd}xCH z0bGr^WM29pB`_(4HdbGGEpdrywx5j27mnlEupIrrw*_0!vELAj6n@gZQ(@Ca`V%~@ z?cqhKvHObHYt~WZRABEtX8J>yP6TG&F$rsk5`$VVuHnn&{Z9U_rHYg$`htf_!p-P3 zDDw+473*KMyANChRGM41)e@@O%=WbvMJ3K<;0Q_%MMz;$8omY*N}kpQ&%J{y4hT;q z>4NQNIP=kzqL-%^KzR&ipmA$Jg}MWwVoORe46Gt*CC;2xj}avaSp=m7y#4zj{iHDC z7>fpiND1p_OJZB5p^+s<{mseRORMDhW)yPS&gcV-|K19^r3he45|`%mzz!Q|PDgrs zaZlu~`m1_jWXdFjVJDbZI>SO`{wFKLZ)YKBHft{jYge9@dg0LM@z=;tmWyxW#A0+T zX!hfo*-P~CVI zsWUZcB{Kz{q#C1KyZ|kOL*R9(9AREoIG`+p!Ho~&GXHL#8?qtCELkO5cMcne zhQLW;)QVhm21(;1w_{c|bP)1;Emc*D}vhBUy51v#u-mL{m z$t9c#!Fz#b5W*@HfZ-ZG(i2p$+u^H}F9i75|2=&My}^y?A7n5MEzW;YMnYQPkKABv>t<)a<=65!V3wg9DE|C zMwRj$nqL+#Av&vW&_ntg-bVOe2Lz}M%v2r+}!>68|L{IM3d+s|@?&Vzv|gSN%yX_cV=1GsKhmT}Zo? zJ@;;=M%Fmcx#)J{>n3Oubo?Tu_3^XSWlCiMYQ_H>~Pj1@(WJOkTK&YIm z231D2Iala6zK+w0(Uyb7ngmw_dQ!7oHg$_aLWI5wi`NJGQ7P|&ezOaq#MkixBF$DZZ5m5*idny*+>OTiD0nM{RbMGZ2NQdez^**Mnd_**swCTY>`c2uaHHMVxw0#8s>rcrZDJ$ zqda1E7o>Mb4Jsub_8|2^LDQnvYHT0BuD4*nf^?;GU3H~f3rF~I-n?oJF_i)Xw5CId zMEHgmoCyxH%=(8+?plpTe={$WcHm=tL*Ml*nLRA1v?{7ge1?#M4j5?G{~G$mU^_fJ z6F_o_E9ep8r=MCE8(J3~!MI}WjTfdwOX$5hJbJigvX}mIG}L5g?>_!RsEF^nzivZ- z@N&36*oPA2n|rFy5QzL4YK;R$=n|=p{g_l@_GCZt@js^+95wuRl@_umy@XenM2-== z78V$zy@1`07SsOHD4xfjf#O7*z`*~9$m1`9X9~I)nr_r9#@Oi24&+Q2VBr978m7rz zGG)$+GQzXsh*Vh6e)G@aOteE+;#>Bijx#-Qv(1S7G&5#^AsOS4GR@9;jjD$r5wX|f z4nurD|A1XEaaO3wL|=3=%oDz4<-)3WsnhbPvLFi%{Ak-{*<%eGrUZQ(jsOF<-);bn z9~|!M?*mvl-6!Fn?BEIryv%pel_|6Jo8p@WoM&oYy(|f^Y(;aSo7z;ZypL%!v3X4EP@nnTg`x7jFjrc;;15TZ`j4ime-t|6qZ52$5~zr$ys|SA*-xZmXpN;XF?O z(}5abnYJ=5U`z_)u9G}kDaVfk-T)575%@YROA6;RCIn1yMrt|JF*2|Xzkyq3#J*_r zF;IMVJ`FWpb61el5dAu`l=jrOk7ROjzxDi?k1&qKi@i5T-auklL$uGrKknI}ttRuP z=jW|4xh>=Wa{;O(mGHbbaz;^YQQ%l34!;90-IEDy^} zzd!q&pSD={&7f-5v}Y9m{4M_>cd_L$TY)YXMeMhho5DUy%5j1FTOqiwR5J-cD`7&Q zUiqaCqezuSZ&E^CAw@!KTZ!JCN&;?d+F_D1GqKJWTX)AX%X^ddC2Q~OeEvnqP^j?K zV|A4K+E|}kc@o_JLdj7G(#WHhpC7M$gJkLSM@*QL&XZR<*@(>Bw~+VbXOKZ6dNuS% z^K0cL;F_UsvX2`>;UiO_`aY8pVFHg{B(~DUZz{yg0YLx{hxmUhqrAg`)~g)AkOe;4h5<3*z3Bi`bh-PlOmoPWWrigUju{urw56#xKIn5?dMV}@CnxzKmQ-@ z4rG7y{l@l9^v%sWfxsjUIp+bJ%msC=k&aN(TQ0702UailL*`Q>6QsCfI=0Vq#a&Ny zw%6`BP=T3u=AY%f!1j#%j=IbYorhoH0r7W<7d7}Z3He{-RhEYI5R?;A7Pt&*KGW-c zL-r3DVP^ZnAUGumHUub)63~J~9iT!jGRF%Hc4%Z2y z9?9=I>oBwYUv*dg6=n4FpA9;e?nXi-MN(Q)Kv3yi5lLy3hNTpwyF*Y!kdT&KLJ{c@ zq+~%#z-0+pn)l)Reb4(pynD`b&i=5^y|Z&?=FXk_nHhv1;%Gf_N;jHS{{GWtilkE?^n_;herXvY-VnfjRq!nJHkwWGO%&!YY=F7-jMUnA*yAzMT*zJ`%A% zWHaJqiW?kzoBS|3Z>#fcBb9`eoa6n&D6jD+AyeEKceKk( zM0%tawNm&62i58u5}e>Ifr*h+GxQ?%)GqwTu_z}$Ie?-h{94}(I(6Z(ZZKs?px8dD zjOHM#umSds|ZWZUPq~`bkb-rSlolk?4Vd1^s)77-91?&hvg>do*Mejq^MNoW6Z- zNGmM)rVwJd-#i5n3xbbGivr+@$z5T)39+QU%B?`RqCxn7)^qXw`^gG2D#fyMrI!8kG zvm-n4D+8zPauE7r3RaY?!$(&`3HCw$fm>2lmPU?)V@!!ZPnv1h`~l(TV!15ki)n&( z5=hNIvAUEKUxH|Lvqy{;GlQa*x<7geG;#n#2SL&mRHmf%YqpWxAv1+qZ!)l=<#-2I6JSt6tt+mT6km2D=B6N>(-RlHj!UUf6 zjnA#dpcfzrlTm=^ra{d?-h%wi%8Ru|psAmcAtyc(@pE&B1@-hflbz8u2mDH3-C-76 zuYDjHRuNXn>($BM4^xEmg9zVcxt_lA&23ezzf~m4n5D0-`9ECt{s^HG3~0~&ko55| zCR!XhK9>UbAqpWp$O&Bi=KPc$IsCMw9M(>Cx0uyF^@4`|;w$g^H`@#=)Nt*0Dr{P$ z`E4+SfGlo3)=(uFwN3g(c56Ru1}%}b(_+fL5!;NtGtyps5FhcY`I#Z{#>$zi(xAY3 zaC~0s^p3fL_z3Qkki9C<$ZY_Cf`|5)1*A2061D}`>L!JicL=hH!WhHYt}zc>Ye@ch z{s~R+^r%&BZizKtT(=fXMLU{CnCRcmzve`JSUe37L-at;pM|CGzdG4xw&C99MP~uQ zuw)8$`k{UJG&tm8S+3j~*<8s|z0TA&9k**ZVay8I>1sDtL0j9vWifLxUu=8N;Y00n# z%b8k!r?@s`@7KhRrKD%X(i5|eST_XlO`^Y<=AF7ICZ|GDySM-g*V*C z%<~vP*y`NkXWJFLoyB5Xkf8!n5J$B}+d@}-fM-R%yBcLAQ02+XfX?~nUqD~bLiNq)Cgch)nAwwqj0?Mr)6+5YG)w5a$F0bqtHgr2koAAft> zB*K{vKY$dd<&b7;KoTK!JEl)1F50<-rAw%^m4AHDq*g7^%K)xS@)*!g4;*KWXf)-$ zyvbnC0po{VdMtU6%uIbitE0bY4J_=7FJg!=)#%)hu{MoV)@?n1P z@b4z*!Z2TV8`<6F>C{g_uSzzAVfBQ{S7MI?-=Z%XDISbKldIi2VoiMih@%5?3-#x& ziD`T&rx*Daq-^g>3MW-NA;`MgV68&+?A9$hdP zV*cjFKS1!n9%gY+2RRZDLxjF^N#OZR#*7ZQ;*7%a1|9&(KwgnzI4xh$9V9KML$J{* z^9aHT(}(HozVG8lWY0Z9Z_XL(l6(DAHR9qt=KvN)_+uY~Mn>Kz)SmSG4kXt8M0i+8 znc=4rtPmqwQb}bBJ0md-n0p-)l9T(+PM68itxVhe_SIqKv9GaF8VKi0gpr{ZVTHdF zT~r{s$+E(2&p!*}>rBHz?^GxQ@cgmd--q%|nrgqJP(Z2md9gaI$=%9L@&Xnav(Umx z{!oYcYF$SERv{i&M<{!O5W7U4?8DA(3;Mph!)*4HW{UkI{V^FksgWVPpbG`*Q9O!x zg@ub*bkHhSg}CpDgJo~U5$|^`FrT>x1ZMI>abgejR{B;92b}&huvKR+XiA~|KX&wn zi7p^*J6%hQL>DgxYPB09Z)Z&^U+yoig~G|NSo|CG(wv>yMwV&FP7^*R9*{MUNA%c` zoOv2oKj`ORn|tW#T)YSaPuW*{55|)Y6zv~4N+$5f@(dg{YXP&4bAR>w0Hfc3ZOu3YQ;X*0Zb>|?HnI0CcA>Y3VU{^0w2+8skBj5 zIvlXM#-lLc@tP3L9sk?R^6Is_O%MZ$viF|yvD^%J)?~PknlpB%oAadwJLbOKefFz& z3a@t+ae&d3y`Q;~er#%;L`7ksni86V#~n!w4MrMmP}I>RaYFeoS@xV^Lke`jRi6Y?@%ciVFx6j_RDv-?(IUz_e=-vq z)A=q+xdSRf>2Q_c93mh;FrN_Zgvu0PP}kyDtFxML5oc`;W%uGzPQW)dPS}ZFt55x3 z#E#9dYBj{*|7N@TSG0~Gmd6#EIb!PdBf16sFM94$?u>e0iS~I|n3fFY#3?g@vOBMW z4$K21b_9)Ux%`##w2rv|hIhSyXV;4ouU`16oLYfVY4gt;E;5o{%%3oK9`53cF{hazi{_UcuzKJn)Vn>oQ@}npB z6UU?`R}_ZE+yS<(H2_WtN?{zB8V1GOJaC3gzTXp)30sM%(99z^W7Sf>RezI}0!)DF z^bUw~Tlr^KE+af@p;wF&1%t;vf&9WRL31TG6CMJ?$b*ve9Zh7C*bJBgOO-nM@aKL)eLx)DUogL!&7&|&rJthE1nWSmD`b`mG6s1Znx zGuwn0i{iEt5AzWCQzdKTJLA({Z}KoL+IIi*u-G0Kg^)&MGD zloG7+av}HtPG@uJ@lTCaQ%c9>AoJ`7Mgm+fOLPF(h>?)XWIGzz&_nm(mD}fp(TfB5h zip})SkMiTbfLoqEY#M94^A5sBf_bqaE#LG+2Y_A0&b7dzJWyk2jz)+FW0ilGBZx(HBBa3NLf;{V}`nFt%`I@SMrXAkFdOd$>z}gIv)%5M06mFR5 z6wz+@T4qgyZ^q`u(t~00mb)nojGztlFst%!yM_sR#-z+TK=jt|?h8uyxT*ofu2y7I%(H+;33%v%d2=5_TsYTnvp}TxzT$Huf(kmLK8<0 zaFsx%5aDEIs*~hVa{0gomR|S(u}tV~e6;(uW|mMFej$sC=zn)(g5_Z8n&d{0b|LH% z&JVF+%3J*4ND-%!Lt=Hf%=e+u&;Y?s1yh#hFHrwqLMhm$CbP9Iblvyo6?`7auxIH>BTt;G(u;boMgzFVY09A9e&!*u9=B8 zAFgi0p^QBE=KGWoP8W`e|4=Llf|O3Fs2TGoVIr#abHc00Rmtn|VV4>&s)JEEBKmKr zuG5~waN7+#`XWuNm!$}SzsjnRP+;nJ6_V=P=dIY=v?qm(AeY&14(Q)hLuvaWP?8{@ zPbCwb{(B(DlgG`Fuo4O~-mKRC4Am}TC8+;m<;j^u5Fm`RQ62^d{`F`KLKJ%2Ni^wq z-0^iRD}>d8yFH4IjIu>rOt&c=7gdFUg;p9u@B7oZ!GK z$VzMF>tH^NGbCU9$|$d<$L8k>%$=19Z;AQt?w&aN zw!$_@UX1Tx7oBE|zJ!=*F)CFDIMZ=uUo;)q-0~km@x8Cwo&GX3!2f|$D7T(ST;^hJ zu#2pmcU_TID-?uv3^=r!E)^|X4y-|Etva+856L>) z7w*AeLASuE`q60t(dG-W(FXBztHEHZ1<|=+($r;pe^uW~Xw9>S5F8nX+CjwndY7c? zaL)i*4@4eqIruid1oDzD{sQxM$fJ^Pb$vHsqs0)AQB0aNenj@m9{FJ*8<`XoY%YYQ zld9i&L!-(zt)hHCT?F+~D6wSu`~zL0z}pBnGpRo^V$9TXx5kRf>#uRQ&I@r-Y{)d! z>Cvds2vnXBS{uT>wk+UmUE!mhK{3-pQll0?FvB!6b#^^hB);&p|33``Dx5H9t(UzsxP z79f)mxqY#t66*F^wLn(tg43gXW#~b_%6F>35UrFVYja$?hq& z>FmLdgKS_Sq(TQ?>o$3yhsI9Cz~h}8)s(-Kl4P}`*2}aZ@oplk{3oUy;^kXkdjy=IYg?tT8i>XQ`IPger;OAJgBaRNntH zRh1jxZ)=u2UiqSzoY*s7BU7cxvZRJ9e5ThNOo69@SSZH}5|49DFExd>Dac_wvj6d^+Hd-a6-Xh*#8$^erG=jIw+7A_%ZU>+KgGNZ@CYu-K{0*^8G9|`=P0DGxH}T zav-j@bFsu>Wi#WV1A4op`rEHTRdM+QDHI znRns#H2p}P86!kMXP(lsBFS%x?C{+G?`d5$8KEm9=VMeu5=z1f0Y;ec zBNvvng_h<&MJ&AOHE+<1$+@wYFS-vxxgGpJ=K2v9k-vSiOU$HMrY0>lH@$_VYhd?G z#9Z`P*rzQJJNNg_zClkDECM2_Oyh^|$Ck=c+XjJW1lH8@aarCM-D^G!?&3T4@zq<^ z2K1jGGB(AcbasRQU4w%4dyn0K`^&3y8MiBkuvQF({XgQ<(Rh-m4_?W{$8rW_ZCE&Z zd>ts#1?TUI46GrkJ!{AQRyz$VTEb;8U#1VaFs8oCoT60AtFBquwT;TzPhaYOGzuUe z*?;de!_6L(<>=hnaAvkKA&Db$%Z!G=NZEj*nukkPG5(Q3Q6_E{;|ESsJ9SyQRbWZb z?!RcukEe`}rB)YdKG;nK^MAy#0ckNlLe>!7YX_c6;Rq`8i&I4R|`9aysy~#lvN*BeMfs)4s(ZeVj_wh`1UN6 zwI)w&;1=0#Zl~4kr0Y2?>0__wzs(}wC4JU9gZaH0b+NtAlP+``R8)Y5C^KvKE!%KU zYIiE+PRpWWUc8CM$`n5h@NME5{`@ra$Dw+j@Daa9M-hvsV;>_v91iGQ!$Q^yz~Y~& zRc(E?lKXKjJz+X@v)88SRDjVn2^G>$25+)PE1E=IZ+u>LO|4*cA+9j;da&0tM2FFg zG2ClsbL-jr_TP!qBjJswB&$KKD}cbP6^}U(gpbbn<8J?{)qnr+X@gND*+qPGHAzC) zU{q)!BfgCg;HjTK|I|Ca&v2VDC^%GPP`;$<-W!o~E1GGrc^MpR`cbFO-T!YoTp;ZD zxlZmq+w3_|%Euy8Mj2CDd$E#{^Whn4H_TF+-fjh-io zN-><|Ig-JTr}0uv88*g&(cR`77AVhfUW3*T7lTR!l)!j+D1-wxRdDL%eY#~fb37uk z$%D~~k@0A{CCUGq3vfLBB^3o0rqu#ZX%?5p6T>1M3wgPg3w{t>b);M9XG_;zK@WYN zbj$o6SJMJnKv3e3X&a*lJMM|bW`Gw2gji4G(k5aV?B zs{osxKen$u?^G|lFB?9eQF{Mr=f!t+-{#$F|UnuRuYlj2zkK=xtq3J1z^gDNt+| zp~(Zz3~FF1HC#2jmlKX4eUk$OP%8+$2*!U!L?c znZCJNd2hg=bo|p1edO42*6_tM0g#4EtGJ^N7+GWla(dLEbbY1!5KG@=sAWiV3$F<^ zA>n^hW}clq+GWGG#{$&TXr$p`iNVi9yk~@z2EIOeKTSb*MzI`7`uQfkIs;H2$kuQp zF4_X+a4iFBLC}f~I(?I8_tl~0#E3?Y(^`8Y#Pn{+@S;25%lEKwrGwE&OXPR&(;*f=9)@yZ!VAW`x*m1-#V2 z>k|a{`W1^70Lo~F7y`6iS9Pac=gdIa?D6WK=k(G0a3w$_a>L*$WjzlW$f#-~TLLl7 zHX%}Y`BFTMUI+6fx5R=yzY}2b8wOK$*{QNv|W~48J z4D4y~wWCmrk8#z1E<=D4_--l3kVrdaU5Hk>=xDtTI9}$tlB$~+-)lnz>6X1{qA$+> zTe^P=(~NxB##S==3YpbSj=s3x_Js-1vgO5f^5j6<{HAn<)F-;CvlL{juvF0^CXP3J zs=9-*WpF5}opXn-E!LFx2sk4C$(2ml7*m#>lsC&Lt}F(ZG4`ObjZ_6JiFxMNngx>Y zDH<0A*T{QF6zxr`GD$9KEX|%AavKT^7)90EPLK*g3J6N`|2^~u0U3`Tb{wCiv7_ts z<0@^`v^LKOxu;7ehfUf^m``{~+j;ZT_}N6!Z9Tx$d1u_HkVRyf@#Uw!b zKCj`?x{3OFm`@Q-^z=ESRP7`wnG`jE3+<)!4b}~Y5-V}wUlHj>311kXzE;=25 zG~*=}Un$KI!s(Y(a}V=Aa%8?`+8m>ZRC(juyVoNDC}`O`A=%&fY$(#k$-}4)x43>y z6LyiIbz_dv0uF<@=0(|a1xvf)|B|}{j$+~9eSUTy$^odYNjI?EaIoH+_6Dhc3{nra zqEqce-IWVSfT3z^PW^`|V-{)3kB4B!`HH^IV^guBnwR3G9Zg84WuB;%=5^|fQCVQ4 zjXg4lyI<|b=j)-l4XWP2DD2&n-7s`hSZGnipfeHhxKqw=*bVG^CLXgMM*n;5{z5qY z;FFTAQ<9f36F{2eeyJ6SBN0GUA3GYixd^&AFVCKhzm9^TI6(bI5N?vFC>x#b$O6Qt z4bxx`+DyNWB zPT%uZ1CRihYULp#SBvjt-iK;izMZk0mlE_T^ZYjk12hzF15`CxHECWic+!2Aoe}E* z8UTG#>Nj?{h42CDbG25tXREA$){?cO`J3>?2%74I90}o9FtLFqCrJuwF>9Oxh9>4B z(nyRQ&s6jg3P1`WqEI|ERO@YuV#g2!_4hh^EGJ(xyJrcK$wMof5U30vidK3mayCH_ z>uj&TUse{USLsv+*U+B7Cnz5Dwn(YCSQmz3N8iABBJY74L`|Ue)b}tLaGIj`$&b%A zyeMom-c>MAsPe`Ppd9^|^as@<^`OQe-A6XrK=Z8*MZ`^|Azs!SVW}wXa2?_W{7b5q zhfqFFV4L4(;f#WN#@CuBeLj6b$NaM=fIL0;Bf0wq{V%+N>Ip$a(KQ%q0WH_%+{+VX zFR-s4?3tr^_v?s;6q~xB;BD%%2afp)rljx2b;JNuM4R@;8h$|S#-F{#DQ+U*g)(b( z3_{aVpqZFSxVWU~8WcrA8a!kIttMsQV#wcHzMU!8AnLzT|MG?^AO?}qv6b;CzV+^< z4i|8gl(Zs^$ja8@%Y1&p_fY_(i|Fa^w|;L&r-O>4MF0(*Jgw4bgJd}HFze9rx@p)^ z3C0)AD$pr56m@^QNKdwM$s@)SJwwWV`;|@}e2l8$jyr=2P{Zy=WC|C8uR+~q`$s5W z!ho$biqz%g=mfsFq_42wsO0eKBHdVHa07itI2B12Qg4Zqs!^-)>8%bLYuc8~pA}UQ8n8NSP~jA4Now3aVK;M6xfAHv)J@Hw zlRu91k@rYLiiv620;8ED`YC~Mpf${uCKnYS9*@=xSK?MQWcn8b|Cni-dHk!CwlH*( z`7Fb?{}HzA8{5d;Irc?OXrKKBWrDG(ii81oA`&q8r|z}H0a30{HN)zK|BHT3ia#)c zxS$Zk^k<6jsgq*&g)RtcwO7+*!II!$B##SW)VzUDE-S8NVXZL%xQdKyiE8wgF4i2v zg`D^%Mr q^pxe2@41Qgo{CHSzxdPzorIDgTn82{XTUy%0HCdYU#&_N8U8;WfNq8W literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/icon@2x.png b/packages/cli-old/template/fm-addon/ProofKitWV/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..30b542393e5e0be1f516add269f7d5618dfc3255 GIT binary patch literal 44003 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 literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info.json b/packages/cli-old/template/fm-addon/ProofKitWV/info.json new file mode 100644 index 00000000..53a241d9 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info.json @@ -0,0 +1,8 @@ +{ + "GUID": "7CCFAF65-7C87-46B1-9FDC-D636B3B9C0AD", + "Clients": ["Pro"], + "Attribution": "Proof+Geist", + "URL": "https://proofkit.dev", + "Icon_Color": "#7F7F7F", + "Version": "1.0" +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_de.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_de.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_de.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_en.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_en.json new file mode 100644 index 00000000..f66557f1 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_en.json @@ -0,0 +1,7 @@ +{ + "Title": "ProofKit WebViewer", + "Description": "Easily build and embed your own custom webviewer widgets into FileMaker", + "Category": "Web", + "Features": [], + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_es.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_es.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_es.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_fr.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_fr.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_fr.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_it.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_it.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_it.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_ja.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_ja.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_ja.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_ko.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_ko.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_ko.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_nl.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_nl.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_nl.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_pt.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_pt.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_pt.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_sv.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_sv.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_sv.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/info_zh.json b/packages/cli-old/template/fm-addon/ProofKitWV/info_zh.json new file mode 100644 index 00000000..6ead9ae7 --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/info_zh.json @@ -0,0 +1,11 @@ +{ + "Title": "ProofKitWV", + + "Description": "*** DESCRIPTION MISSING *** - DNL", + + "Category": "___...___", + + "Features": [], + + "Optimized": ["Desktop", "Tablet", "Mobile"] +} diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/it.xml b/packages/cli-old/template/fm-addon/ProofKitWV/it.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/it.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/ja.xml b/packages/cli-old/template/fm-addon/ProofKitWV/ja.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/ja.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/ko.xml b/packages/cli-old/template/fm-addon/ProofKitWV/ko.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/ko.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/nl.xml b/packages/cli-old/template/fm-addon/ProofKitWV/nl.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/nl.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/preview.png b/packages/cli-old/template/fm-addon/ProofKitWV/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..30b542393e5e0be1f516add269f7d5618dfc3255 GIT binary patch literal 44003 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 literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/pt.xml b/packages/cli-old/template/fm-addon/ProofKitWV/pt.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/pt.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_de.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_de.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_en.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_en.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_es.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_es.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_fr.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_fr.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_it.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_it.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_ja.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_ja.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_ko.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_ko.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_nl.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_nl.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_pt.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_pt.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_sv.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_sv.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/records_zh.xml b/packages/cli-old/template/fm-addon/ProofKitWV/records_zh.xml new file mode 100644 index 0000000000000000000000000000000000000000..15efb52c7e7c2348bad927e8ad53977448a66535 GIT binary patch literal 1918 zcmeH|TW=Ck6ovO|{S~C|Lt(fK5VRR!CNW-OD%D3*pePAY1E}@StKZs(DM&GG-_3+` z+n2Lut-Xiezq)pAx3;jkmF&A++sf89vm0C5b33)Fm94^jZZGVabuG6ymfD2pllvTt ziHRwGiY?iDe7})-OnzmJJ+E?C-JWyfd~17$-wn^yUU9mykM;q-DG0yf9lQUfUD`_< zc&^0yJUOUxYg)r1(@BdlvN|U{q9SW!9dI5oHm&C8HEeCpD#Q*v@0@$bc6Em(gN4uR zW^nSwJvOWtAYFs($}xraser6b_Nw1j1;^0W9kM6ZVO=9$9G<}9fLO(sJlDcT&imL| zT4D*z7ygO}oTs1@0;%)ZSVdouU)fjaw%(#R0f}rqaICQpnMeNS@}4@PYwnR|CGMPT zQN?g3O_%tK31L<4#a5*m^Jtq>?-+8H;km~zg?Z@{!$%F4QpOhSw0zYkcY-Z}wFaZ+ zCVO$HJNI?ZT=R^`2AMOsS3W5!-Fxh$cFJCvBK%YnRsW@oZC5OJRl0UY#KSxU{R-wT zc4J(DdC?D`9$vt8B-*y+gnBQB?ihMEjUACHund_*no<9ZU3dDLtbxhGUMQ8n8OAYFxL3E3Jp@Q_KNm#3vRGTrYHklJpMV#l3 zPL(Wt8Mch)zCuCk$JEx2Ro(r+$BG(Pm7l@(ziauh_T_)PFJuXQw;!`REqZxp4Yp%n WPg(ANl|!U##W!9j+8_RN>iiF@?g#S# literal 0 HcmV?d00001 diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/sv.xml b/packages/cli-old/template/fm-addon/ProofKitWV/sv.xml new file mode 100644 index 00000000..f4d36f4a --- /dev/null +++ b/packages/cli-old/template/fm-addon/ProofKitWV/sv.xml @@ -0,0 +1,896 @@ + + + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/fm-addon/ProofKitWV/template.xml b/packages/cli-old/template/fm-addon/ProofKitWV/template.xml new file mode 100644 index 0000000000000000000000000000000000000000..e74664527947ccda6603b96bfc51e9a72b72f4e7 GIT binary patch literal 237896 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 qJ}ZCT@4hEp@tmoi + com.fmi.basetable.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.basetable.ProofKitWV + + + + com.fmi.basetable.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.basetable.field.ProofKitWV::HTML + + + + com.fmi.calculation.text.01F213BC707E4F9F60B5E7C27F11947E + fn + fn + com.fmi.calculation.text.fn + + + + com.fmi.calculation.text.03E36891B1790CAB1C9D28969AFDBB14 + file:///# + file:///# + com.fmi.calculation.text.file:///# + + + + com.fmi.calculation.text.03FD2C4D5BAC1120105C1A664E398946 + callback + callback + com.fmi.calculation.text.callback + + + + com.fmi.calculation.text.07FD75C35AEB713C3F6922A0FB81FD01 + Server + Server + com.fmi.calculation.text.Server + + + + com.fmi.calculation.text.10A2FCEF94DEADE2D286DDA6EF3ED2DE + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + com.fmi.calculation.text. style=\"margin:0;position:fixed;top:0;bottom:0,right:0;left:0;width:100%;height:100%;overflow:hidden;\" + + + + com.fmi.calculation.text.1B759226EB6991D1214E639EA673D087 + ? + ? + com.fmi.calculation.text.? + + + + com.fmi.calculation.text.218F38C07108BC07C8C0F9240A5C664B + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.22FB870EE7AB348C700B68C887AFCC25 + {} + {} + com.fmi.calculation.text.{} + + + + com.fmi.calculation.text.233002B9D9961C9125B509A245B9589B + \"message\", + \"message\", + com.fmi.calculation.text. \"message\", + + + + com.fmi.calculation.text.2776CB5D7B6FBD0FD4C6A6AA444A2981 + <script> + <script> + com.fmi.calculation.text. <script> + + + + com.fmi.calculation.text.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.calculation.text.web + + + + com.fmi.calculation.text.2AC373EA360A4D50ED76C2759130555D + <body style=\"padding: 0px; overflow: hidden\"> + <body style=\"padding: 0px; overflow: hidden\"> + com.fmi.calculation.text. <body style=\"padding: 0px; overflow: hidden\"> + + + + com.fmi.calculation.text.2B0F8A29B6D9BC16986F31701A77F93F + if (typeof params === \"string\") { + if (typeof params === \"string\") { + com.fmi.calculation.text. if (typeof params === \"string\") { + + + + com.fmi.calculation.text.3027BC59CD646F09A354171AA2FC5140 + function (e) { + function (e) { + com.fmi.calculation.text. function (e) { + + + + com.fmi.calculation.text.30C04A9C55F1B1A63999CFA98C2112FF + }; + }; + com.fmi.calculation.text. }; + + + + com.fmi.calculation.text.3A4BEA1DEFD16FE6EB3D2A6C9093EAE2 + </script> + </script> + com.fmi.calculation.text. </script> + + + + com.fmi.calculation.text.46231D675DCF08EA094EBC95E558C9D1 + params = JSON.parse(params); + params = JSON.parse(params); + com.fmi.calculation.text. params = JSON.parse(params); + + + + com.fmi.calculation.text.47B3593A183A448A90437004BC646571 + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + com.fmi.calculation.text.You are about to upload a new verison of the inlined HTML to this Addon. Do you want to continue? + + + + com.fmi.calculation.text.481AEB25AD72DC7585BBBC2E4288AA6A + \" + \" + com.fmi.calculation.text. \" + + + + com.fmi.calculation.text.4AC6BA8868A9D055FCCC851659679C46 + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.50B195ED2E968D0C63A2D6C9B1204487 + data: data, + data: data, + com.fmi.calculation.text. data: data, + + + + com.fmi.calculation.text.52C3DFF69657FB45C9E0A2BC11F34373 + return; + return; + com.fmi.calculation.text. return; + + + + com.fmi.calculation.text.5327FDB566A4B82BC52735114CEB4BCB + ></iframe> + ></iframe> + com.fmi.calculation.text. ></iframe> + + + + com.fmi.calculation.text.5C1699ACAFF841AF287498489285A20B + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.5D9C9F30268BF1BA6DA9EDAD76536C93 + id=\"app_iframe\" + id=\"app_iframe\" + com.fmi.calculation.text. id=\"app_iframe\" + + + + com.fmi.calculation.text.684F95A306F44B0537585BEA86979FEB + /secondary + /secondary + com.fmi.calculation.text./secondary + + + + com.fmi.calculation.text.69D96751330C40C4E438780FF86F2615 + var target = document.getElementById(\"app_iframe\"); + var target = document.getElementById(\"app_iframe\"); + com.fmi.calculation.text. var target = document.getElementById(\"app_iframe\"); + + + + com.fmi.calculation.text.729D67BCA45FB5FAE50567FE17377BE6 + data:text/html;charset=UTF-8, + data:text/html;charset=UTF-8, + com.fmi.calculation.text.data:text/html;charset=UTF-8, + + + + com.fmi.calculation.text.7939AE436E09B3990F24176D655909BB + <html> + <html> + com.fmi.calculation.text.<html> + + + + com.fmi.calculation.text.7AAFF5F311B69E3E224DDA3C8D2F606F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.7CAAAB98E591AB463EDBB77E292450C9 + </html> + </html> + com.fmi.calculation.text.</html> + + + + com.fmi.calculation.text.821D7341CD938C0470F50650524AB3F4 + src=\" + src=\" + com.fmi.calculation.text. src=\" + + + + com.fmi.calculation.text.87DA46E8399540912B99DA91F9AF6AA0 + target.contentWindow.postMessage( + target.contentWindow.postMessage( + com.fmi.calculation.text. target.contentWindow.postMessage( + + + + com.fmi.calculation.text.889D001E7116D30178B1966E155DBAC6 + The new version was uploaded. + The new version was uploaded. + com.fmi.calculation.text.The new version was uploaded. + + + + com.fmi.calculation.text.8941760420115B72B31334BA82F15A8B + otherwise, load the HTML from the field + otherwise, load the HTML from the field + com.fmi.calculation.text. otherwise, load the HTML from the field_CR/_ + + + + com.fmi.calculation.text.8C487FA0558ABEF4B27C8E95726D4A03 + http://localhost:5175/# + http://localhost:5175/# + com.fmi.calculation.text.http://localhost:5175/# + + + + com.fmi.calculation.text.919523ABFEC76214004FCD3B43FCD75F + try { + try { + com.fmi.calculation.text. try { + + + + com.fmi.calculation.text.9286618FE0B2C104A3FBBEC4C6CD20A4 + + + com.fmi.calculation.text. + + + + com.fmi.calculation.text.92888813B9BBF2454691E3BD3E39EFE3 + New version uploaded + New version uploaded + com.fmi.calculation.text.New version uploaded + + + + com.fmi.calculation.text.95715311B52CDD7A417B0223E5B0100F + </body> + </body> + com.fmi.calculation.text. </body> + + + + com.fmi.calculation.text.9C6D318B6298E73CE7F9D36D01656757 + // if this a string parse it, if not, use it as is + // if this a string parse it, if not, use it as is + com.fmi.calculation.text. // if this a string parse it, if not, use it as is + + + + com.fmi.calculation.text.9DB0F0B8A4045E59AB42B2497C32D41D + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.9DF8FCE4C5DEA8984C365A0B8361A856 + <iframe + <iframe + com.fmi.calculation.text. <iframe + + + + com.fmi.calculation.text.9FEC8133ADD65209C6115C2AAD70B8C4 + functionName: fnName, + functionName: fnName, + com.fmi.calculation.text. functionName: fnName, + + + + com.fmi.calculation.text.A009BCCF13CA2631D3982CD37FBDCD8B + hello + hello + com.fmi.calculation.text.hello + + + + com.fmi.calculation.text.A23FA8E5B8EAD4001D5CA05DD4534E62 + fetchId: fetchId, + fetchId: fetchId, + com.fmi.calculation.text. fetchId: fetchId, + + + + com.fmi.calculation.text.A262D3A8CCAD06012DDEDFD393B622DB + params = params; + params = params; + com.fmi.calculation.text. params = params; + + + + com.fmi.calculation.text.A2BD7D3FFE632D8458623034627F039A + window.addEventListener( + window.addEventListener( + com.fmi.calculation.text. window.addEventListener( + + + + com.fmi.calculation.text.A46699D96860EE56B35816EB92EB5BCB + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.A4BD007639F211BE8C2368ED49440F3C + fetchId + fetchId + com.fmi.calculation.text.fetchId + + + + com.fmi.calculation.text.A8EC3F7E5DB92687357FB5D8F267299D + Uploading Inlined HTML + Uploading Inlined HTML + com.fmi.calculation.text.Uploading Inlined HTML + + + + com.fmi.calculation.text.AD433063E863626B9E7515977206C11E + \" + \" + com.fmi.calculation.text.\" + + + + com.fmi.calculation.text.ADED719D62CE7F8C994BEAC680EA5CBB + ); + ); + com.fmi.calculation.text. ); + + + + com.fmi.calculation.text.B179E61F4E5F755432BFB89AE4688765 + ?useWrapper=1\" + ?useWrapper=1\" + com.fmi.calculation.text.?useWrapper=1\" + + + + com.fmi.calculation.text.B1954CB690053A9748E0AA605BBA8806 + data:text/html, + data:text/html, + com.fmi.calculation.text.data:text/html, + + + + com.fmi.calculation.text.BC1CEEA5C56513848510635337E717C6 + FileMaker.PerformScript(params.script, params.data, params.options); + FileMaker.PerformScript(params.script, params.data, params.options); + com.fmi.calculation.text. FileMaker.PerformScript(params.script, params.data, params.options); + + + + com.fmi.calculation.text.BC60F87E4D78046DC14562853A562493 + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect) + com.fmi.calculation.text. wrap everything in the WebViewer compatible iFrame (only takes effect in WebDirect)_CR/_ + + + + com.fmi.calculation.text.BF260A0069D595A9F6A99BF63FE49CBA + webViewerName + webViewerName + com.fmi.calculation.text.webViewerName + + + + com.fmi.calculation.text.BF27CA10BA6CC8943349796AD49F4166 + result + result + com.fmi.calculation.text.result + + + + com.fmi.calculation.text.C46BC5CA12F543589842A0706FCFE2E6 + world + world + com.fmi.calculation.text.world + + + + com.fmi.calculation.text.C5937366DE87E3BE8DFFC600BCAF1C52 + http://localhost:5175 + http://localhost:5175 + com.fmi.calculation.text.http://localhost:5175 + + + + com.fmi.calculation.text.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.calculation.text.ProofKitWV + + + + com.fmi.calculation.text.CCE22DB8E3AE0D08AF6060241C26A527 + frameborder=\"0\" + frameborder=\"0\" + com.fmi.calculation.text. frameborder=\"0\" + + + + com.fmi.calculation.text.D36D520AC81E0A72AE7416332519D6BA + console.log(\"posting to wrapper error\", e); + console.log(\"posting to wrapper error\", e); + com.fmi.calculation.text. console.log(\"posting to wrapper error\", e); + + + + com.fmi.calculation.text.D51A8CFF27F08805EF9EF825D4D19627 + } + } + com.fmi.calculation.text. } + + + + com.fmi.calculation.text.D84A2645225C2EE655990CC01D1AFB69 + callIntoIFrame + callIntoIFrame + com.fmi.calculation.text.callIntoIFrame + + + + com.fmi.calculation.text.D8C555C07E9A709F2A6840605C548F0D + console.log(`bridging parameter incorrect: ${params}`); + console.log(`bridging parameter incorrect: ${params}`); + com.fmi.calculation.text. console.log(`bridging parameter incorrect: ${params}`); + + + + com.fmi.calculation.text.DA6A6365DCE98B906B857DD8EA6C3F93 + data + data + com.fmi.calculation.text.data + + + + com.fmi.calculation.text.DCDF1E9F27F0708E782858CEC973DA89 + } catch (e) { + } catch (e) { + com.fmi.calculation.text. } catch (e) { + + + + com.fmi.calculation.text.E20887F7B6D94D2747941C5DBDE31282 + }, + }, + com.fmi.calculation.text. }, + + + + com.fmi.calculation.text.E378E40E115D4E177936A4D9EA3AC626 + false + false + com.fmi.calculation.text. false + + + + com.fmi.calculation.text.EC0879E1C77B7C2D0102C5AE125B83AD + <!DOCTYPE html> + <!DOCTYPE html> + com.fmi.calculation.text.<!DOCTYPE html> + + + + com.fmi.calculation.text.EFCCEB1C6E8BDB197EA2B6A8A9B07288 + let params = e.data; + let params = e.data; + com.fmi.calculation.text. let params = e.data; + + + + com.fmi.calculation.text.EFE4D1CABD13E7E24F1B636BCE597CD9 + if (params.forFM) { + if (params.forFM) { + com.fmi.calculation.text. if (params.forFM) { + + + + com.fmi.calculation.text.F0C67B2F92DBF5CA2CF838C833FD952D + const params = { + const params = { + com.fmi.calculation.text. const params = { + + + + com.fmi.calculation.text.F874F96732933F16898A065A6DC760A3 + params, + params, + com.fmi.calculation.text. params, + + + + com.fmi.calculation.text.FB21805207B804DDE591F825C88797A3 + } else { + } else { + com.fmi.calculation.text. } else { + + + + com.fmi.calculation.text.FEBA1CAF0ACFE35666E2420EFA48E0DE + If this var is true, we want to see the changes from our dev server + If this var is true, we want to see the changes from our dev server + com.fmi.calculation.text. If this var is true, we want to see the changes from our dev server_CR/_ + + + + com.fmi.calculation.text.FFAC7088652C1EF5FC31B52690768F79 + function callIntoIFrame(fnName, data, fetchId) { + function callIntoIFrame(fnName, data, fetchId) { + com.fmi.calculation.text. function callIntoIFrame(fnName, data, fetchId) { + + + + com.fmi.layout.4F16BD3C5BF01CBD7F533942E68FD14A + ProofKitWV_Internal + ProofKitWV_Internal + com.fmi.layout.ProofKitWV_Internal + + + + com.fmi.layout.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.layout.ProofKitWV + + + + com.fmi.layoutobject.291E43F0C7C648CC5F9F71BB3194C971 + web + web + com.fmi.layoutobject.web + + + + com.fmi.script.2731467863E20F0E70D999E1F9C8D428 + Refresh Webviewer Widget + Refresh Webviewer Widget + com.fmi.script.Refresh Webviewer Widget + + + + com.fmi.script.5544B2E96E3F871113036A2A0F959B83 + FETCH CALLBACK TEMPLATE + FETCH CALLBACK TEMPLATE + com.fmi.script.FETCH CALLBACK TEMPLATE + + + + com.fmi.script.58E94BD3BB2D0ACEDA33387EB3813B27 + SendCallBack + SendCallBack + com.fmi.script.SendCallBack + + + + com.fmi.script.9EB7DF46B90E85D20A5C138216E9610A + Disable WebDev Mode + Disable WebDev Mode + com.fmi.script.Disable WebDev Mode + + + + com.fmi.script.9EFEF284E38F4F6C39884A6253DE7586 + UploadWebviewerWidget + UploadWebviewerWidget + com.fmi.script.UploadWebviewerWidget + + + + com.fmi.script.B23EB3051DFD0902F2C16AC08CDBEF9E + Launch Web Viewer for Dev + Launch Web Viewer for Dev + com.fmi.script.Launch Web Viewer for Dev + + + + com.fmi.script.BBAD42748576DDB1A4D0E96A448D64DC + EXAMPLE: Navigation + EXAMPLE: Navigation + com.fmi.script.EXAMPLE: Navigation + + + + com.fmi.script.E684EC5DAE0FC476921FE0FF357DD35F + Enable WebDev Mode + Enable WebDev Mode + com.fmi.script.Enable WebDev Mode + + + + com.fmi.script.F7662E70CB310C44167F9730EE7B494C + ExecuteDataApi + ExecuteDataApi + com.fmi.script.ExecuteDataApi + + + + com.fmi.script.folder.2D02A6E046E57E03ADE2FE67E50B2DB2 + ProofKit Web Viewer + ProofKit Web Viewer + com.fmi.script.folder.ProofKit Web Viewer + + + + com.fmi.script.folder.54041CFCFFF324E78E4E511B61A7B27D + EXAMPLES + EXAMPLES + com.fmi.script.folder.EXAMPLES + + + + com.fmi.script.folder.CAB285360B0FD342DB052A977FC4FF21 + Private + Private + com.fmi.script.folder.Private + + + + com.fmi.script.folder.E5957734D733284A6D5077C5E5A0B4B7 + fm-webviewer-fetch + fm-webviewer-fetch + com.fmi.script.folder.fm-webviewer-fetch + + + + com.fmi.scriptstep.text.0856E4B6E62830AA9310D82D0CFB6B2A + It is never used by the users of an application using this addon + It is never used by the users of an application using this addon + com.fmi.scriptstep.text. It is never used by the users of an application using this addon + + + + com.fmi.scriptstep.text.10DFE7629893BAA57E72B50BFE96BFCD + This script is conly called from script receiving requests from WebViewers + This script is conly called from script receiving requests from WebViewers + com.fmi.scriptstep.text.This script is conly called from script receiving requests from WebViewers + + + + com.fmi.scriptstep.text.217674D9B26944151416FE172BB744EF + optional Properties + optional Properties + com.fmi.scriptstep.text.optional Properties + + + + com.fmi.scriptstep.text.23C3FED490F01563FABBC2C4EA425D4C + On server, we'll return the callback value which will be picked up by the callback script + On server, we'll return the callback value which will be picked up by the callback script + com.fmi.scriptstep.text.On server, we'll return the callback value which will be picked up by the callback script + + + + com.fmi.scriptstep.text.2482056B7619644EA967036F8675E46E + @param {string} callback.fn the name of the function that will be used + @param {string} callback.fn the name of the function that will be used + com.fmi.scriptstep.text.@param {string} callback.fn the name of the function that will be used + + + + com.fmi.scriptstep.text.310923E005347F529661D9884A470D04 + Any way you know how that doesn't leave the layout (unless you're running this script on server) + Any way you know how that doesn't leave the layout (unless you're running this script on server) + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout (unless you're running this script on server) + + + + com.fmi.scriptstep.text.3CA1E7C24356583F32850B619A6EBCDD + use this script to copy to your own file + use this script to copy to your own file + com.fmi.scriptstep.text.use this script to copy to your own file + + + + com.fmi.scriptstep.text.3D43D095B6DBB2074FD200CC00EA2449 + /## + /## + com.fmi.scriptstep.text./## + + + + com.fmi.scriptstep.text.3EE491551E342EF3376EB68BB36FD433 + Required properties + Required properties + com.fmi.scriptstep.text.Required properties + + + + com.fmi.scriptstep.text.3F504E0E87B3A79711C3DD43A9858D1B + Any way you know how that doesn't leave the layout. + Any way you know how that doesn't leave the layout. + com.fmi.scriptstep.text.Any way you know how that doesn't leave the layout. + + + + com.fmi.scriptstep.text.436FEF45C8565289839D7334DDAB413E + -------------------- UPDATE INFO HERE ------------------- + -------------------- UPDATE INFO HERE ------------------- + com.fmi.scriptstep.text.-------------------- UPDATE INFO HERE ------------------- + + + + com.fmi.scriptstep.text.43F8E1BF6E80CA0F3B3975E839289130 + get a result in any way you know how! + get a result in any way you know how! + com.fmi.scriptstep.text.get a result in any way you know how! + + + + com.fmi.scriptstep.text.53412D1C9296389206C3A134AAD6C133 + @param {string} callback.fetchID the request in the proper api request format + @param {string} callback.fetchID the request in the proper api request format + com.fmi.scriptstep.text.@param {string} callback.fetchID the request in the proper api request format + + + + com.fmi.scriptstep.text.539DD5EA4BA637EF5979D41348EB6FB4 + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + -------------------- ↓ UPDATE INFO HERE ↓ ------------------- + com.fmi.scriptstep.text.-------------------- ↓ UPDATE INFO HERE ↓ ------------------- + + + + com.fmi.scriptstep.text.602B6391A8C8ACF21D775403D8DB46E8 + This ensures this script will always run on server (if the file is hosted) + This ensures this script will always run on server (if the file is hosted) + com.fmi.scriptstep.text.This ensures this script will always run on server (if the file is hosted) + + + + com.fmi.scriptstep.text.6DCF8C508DFDFCE8788EE2A120040404 + $result must be an object. + $result must be an object. + com.fmi.scriptstep.text.$result must be an object. + + + + com.fmi.scriptstep.text.91DD624E3BA0CAD077A8990C590C154C + @param {object} json.result the data to send back to the WebViwer + @param {object} json.result the data to send back to the WebViwer + com.fmi.scriptstep.text.@param {object} json.result the data to send back to the WebViwer + + + + com.fmi.scriptstep.text.99F2DDEA1476D167071C5604C5E3B795 + @param {string} callback.webViewerName the name of the webViewer + @param {string} callback.webViewerName the name of the webViewer + com.fmi.scriptstep.text.@param {string} callback.webViewerName the name of the webViewer + + + + com.fmi.scriptstep.text.ABDB20FE60943D5012DB4C153D2E3E25 + We're not on server, send the data directly back to the webviewer + We're not on server, send the data directly back to the webviewer + com.fmi.scriptstep.text.We're not on server, send the data directly back to the webviewer + + + + com.fmi.scriptstep.text.ADA99C72F369DA4434F8B9EAB407EA59 + This script is only used when developing webviewer widgets + This script is only used when developing webviewer widgets + com.fmi.scriptstep.text. This script is only used when developing webviewer widgets + + + + com.fmi.scriptstep.text.B24D1A82B2420607AD27965854A09EA4 + #### Remove this block if you don't want to run the fetch on server + #### Remove this block if you don't want to run the fetch on server + com.fmi.scriptstep.text.#### Remove this block if you don't want to run the fetch on server + + + + com.fmi.scriptstep.text.B27574B1C459943F6BD5404637E8EADA + You will never need to edit this script + You will never need to edit this script + com.fmi.scriptstep.text.You will never need to edit this script + + + + com.fmi.scriptstep.text.BAAD60E8350FC01BBD0EE47A932724BA + uploads the html code into the right location + uploads the html code into the right location + com.fmi.scriptstep.text. uploads the html code into the right location + + + + com.fmi.scriptstep.text.CCA79BCCE089A932DE2F6BD5B6B89C43 + @param {object} callback + @param {object} callback + com.fmi.scriptstep.text.@param {object} callback + + + + com.fmi.scriptstep.text.D13014675DFC35477384592FD97832E0 + #/ + #/ + com.fmi.scriptstep.text.#/ + + + + com.fmi.scriptstep.text.D5244507E679E5BF60C5AF5B92D220DA + only if running in webdirect + only if running in webdirect + com.fmi.scriptstep.text.only if running in webdirect + + + + com.fmi.scriptstep.text.D71139CD0F3751336C2AFB84F329620E + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + -------------------- ↑ UPDATE INFO HERE ↑ ------------------- + com.fmi.scriptstep.text.-------------------- ↑ UPDATE INFO HERE ↑ ------------------- + + + + com.fmi.scriptstep.text.E0BE07C2519E4D0DFE50BE557EAE632D + ####################### + ####################### + com.fmi.scriptstep.text.####################### + + + + com.fmi.tableoccurrence.CBCA67C2335A3818A3C9727057706207 + ProofKitWV + ProofKitWV + com.fmi.tableoccurrence.ProofKitWV + + + + com.fmi.tableoccurrence.field.ProofKitWV::8C0EC245B43DB2131C17BF7825C4CCAF + ProofKitWV::HTML + ProofKitWV::HTML + com.fmi.tableoccurrence.field.ProofKitWV::ProofKitWV::HTML + + + + com.fmi.tableoccurrence.field.ProofKitWV::CEF66026AC6A040A2A2F2DCC712C456B + HTML + HTML + com.fmi.tableoccurrence.field.ProofKitWV::HTML + diff --git a/packages/cli-old/template/nextjs-mantine/README.md b/packages/cli-old/template/nextjs-mantine/README.md new file mode 100644 index 00000000..4f416a4a --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/README.md @@ -0,0 +1,27 @@ +# ProofKit NextJS Template + +This is a [NextJS](https://nextjs.org/) project bootstrapped with `@proofkit/cli`. Learn more at [proofkit.dev](https://proofkit.dev) + +## 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.dev). + +## 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 new file mode 100644 index 00000000..00bba9bb --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/_gitignore @@ -0,0 +1,37 @@ +# 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 new file mode 100644 index 00000000..0d27c449 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/components.json @@ -0,0 +1,21 @@ +{ + "$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 new file mode 100644 index 00000000..9555317e --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/next.config.ts @@ -0,0 +1,12 @@ +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 new file mode 100644 index 00000000..31fad730 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/package.json @@ -0,0 +1,51 @@ +{ + "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 new file mode 100644 index 00000000..085a0ef9 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/postcss.config.cjs @@ -0,0 +1,15 @@ +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 new file mode 100644 index 00000000..c536f9bf --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/proofkit.json @@ -0,0 +1,7 @@ +{ + "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 new file mode 100644 index 0000000000000000000000000000000000000000..ba9355b8d3f888ad3a93c0f254b6776a0c2aed92 GIT binary patch literal 15086 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 DtPcMKfsh_-A)LFM)6iMLm_0IRoy0b^RZ#+}< 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 b29HP@D9EFJDBd}ZfIn(VT8gjb%^v(OKI6J9 literal 0 HcmV?d00001 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 new file mode 100644 index 00000000..57a8452b --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..0f180c04 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx @@ -0,0 +1,90 @@ +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+Geist + {" "} + 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 new file mode 100644 index 00000000..9512cb63 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/app/layout.tsx @@ -0,0 +1,39 @@ +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 new file mode 100644 index 00000000..887073db --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx @@ -0,0 +1,12 @@ +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 new file mode 100644 index 00000000..f5ea4966 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..8c4270df --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..79d81bad --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css @@ -0,0 +1,40 @@ +.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 new file mode 100644 index 00000000..4409b1d6 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx @@ -0,0 +1,34 @@ +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 new file mode 100644 index 00000000..910104fb --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx @@ -0,0 +1,27 @@ +"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 new file mode 100644 index 00000000..06ce2676 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx @@ -0,0 +1,35 @@ +"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 new file mode 100644 index 00000000..ded639d0 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..2de3b630 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx @@ -0,0 +1,13 @@ +/** + * 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 new file mode 100644 index 00000000..781fcbce --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx @@ -0,0 +1,23 @@ +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 new file mode 100644 index 00000000..9943f8a0 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx @@ -0,0 +1,43 @@ +"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 new file mode 100644 index 00000000..6c392c95 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx @@ -0,0 +1,26 @@ +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 new file mode 100644 index 00000000..3c50ef8d --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/config/env.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..0e2f76bb --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css @@ -0,0 +1,125 @@ +/* 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 new file mode 100644 index 00000000..890db89c --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 00000000..7f62198a --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts @@ -0,0 +1,3 @@ +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 new file mode 100644 index 00000000..b5aa63e3 --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts @@ -0,0 +1,32 @@ +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 new file mode 100644 index 00000000..1f4cd38e --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/src/utils/styles.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..51d0dbce --- /dev/null +++ b/packages/cli-old/template/nextjs-mantine/tsconfig.json @@ -0,0 +1,27 @@ +{ + "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 new file mode 100644 index 00000000..869eaac0 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md @@ -0,0 +1,327 @@ +# 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 new file mode 100644 index 00000000..98495535 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc @@ -0,0 +1,333 @@ +--- +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 new file mode 100644 index 00000000..1043bea0 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/.vscode/settings.json @@ -0,0 +1,35 @@ +{ + "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 new file mode 100644 index 00000000..4f416a4a --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/README.md @@ -0,0 +1,27 @@ +# ProofKit NextJS Template + +This is a [NextJS](https://nextjs.org/) project bootstrapped with `@proofkit/cli`. Learn more at [proofkit.dev](https://proofkit.dev) + +## 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.dev). + +## 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 new file mode 100644 index 00000000..00bba9bb --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/_gitignore @@ -0,0 +1,37 @@ +# 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 new file mode 100644 index 00000000..3ac108f5 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/biome.json @@ -0,0 +1,48 @@ +{ + "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 new file mode 100644 index 00000000..ffe928f5 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/components.json @@ -0,0 +1,21 @@ +{ + "$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 new file mode 100644 index 00000000..4e6fb1fe --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/next.config.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 00000000..a61be86e --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/package.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 00000000..c7bcb4b1 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/postcss.config.mjs @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000..13d3916d --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/proofkit.json @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 0000000000000000000000000000000000000000..ba9355b8d3f888ad3a93c0f254b6776a0c2aed92 GIT binary patch literal 15086 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 DtPcMKfsh_-A)LFM)6iMLm_0IRoy0b^RZ#+}< zi!bLut%2o?|K58mAIkLAGYnAH0<+q{5T*Z_{;{|^gi0Jnq^GA@JxQqtmY9eq8qay! zO9PJSSvYy5L-*71;XvO?minoHci?`$*ErTT)r;&5d)*7fST2hTv9C=eK-j==kky7D zAUwps{~#iyWjsMZphl3BeEG&bV>{Dh##GAXtaUsf^jqDFNEn%wBp)J$iXMX03+R_G zb?kWBXzMAG_@mNM!*xye7Q>b!?U8L<5WU2E{b`mrTvAH!pztilb8Ak` zWsm!)zfzsOb#HBS^k$}HaHn)bv{i8JjX$n9I!)mJ`t`(34~+cJFiXcr7!0$Hy&Q5; z$6p@hVQZIVkL*)bA>+$)J)mkAhk*ANC}Yeg&YovT*bh$8ToHF=Xh2D$Yj-F?PwKHj ztzLSgR9I_7?+`I?|K9Z3hnKZ(%9w3@V3Vx)6#-N3P3^nXL?d=G4?ix7e&gDiufyIO4-KNBEMFG5y_ zGC8=L@Efm3B+i~NMD_xid=%!f+=kmoSGUQT)cO+wWy$A7##<{=J^l=21g41G1q}cPAPn67648JBv8iPX(c^$iDHgB)#7-D=-rlm)^k7 zXKI!ue`8CSPe4WHX99u6k^M$g!Y5y z{h35-ws0Y~R%7z`!*5A#2;Z zpDjS06yr;n0>A6~>H9VR=$V6hBi25l`-86VQkKsN3S0y#bD6W?M*L3Im*<#Z_=6$h zJX#B82E{2(LvxA03{ng-CC-e+rwM}u>woOx7odN>ukb{k$Yfu7!PSAi+7rQFB9pdX z(nwOGZOde~xXAnR%nVYNp@;r$kP%BSx|O}I$w3Wbq&^PSq^vnlClX@X0A8>-JPD$n zViz&^EzGWi?=z_MdPu^Rz*M0EJOH|Rcvab10!ydN9~ObS<@~(x+!X+WbpcDT0&`+E zY8)SuVEko9oTmW8DEzWlYjT!tpmbL`3wNu0rZko`;(uEiU-k`z?6nh_!*XsY&L1{% z3e(miJ|zQq!4Dy;5csp##f(@jBDBn3bB)g!68l@SM^kLT(=7mY3^2R?&ukK4>k>b( zIkyk$wT(F$QP6{bSdiZHd^uau^(PgUZ|YRA)E{kL<;335TwCUQTk!{m$97!(%^o7Z zd5zKVe?0t9e{*!1^J&Po6kaS_5Bz2!g8eFH8lV>_zzf%uaS1j zUN>les1aB`&bI$CdHU@S!`ijuD|W;HB&Yr$xghJuiuaocqUZpvzlqS%D}+1erDeYNBtYXQA#)1m!iwB&hj+5YVK4EguookgEW{|N}d z&)?c_JpLO~@nX_fZIAzPY^34;_+c6itbYOgrZZ5B_YLyj9uN3QfcUrV0RVja?VwK7 zznN_chme(%|Hk=$gY=sU+^>IF{a;A^rYltacQ8S(u>SuKXoVCQSrYIDTL`ttKJ!#q z-eAwbCk&{wb31;pV_%bR7yTVEt(j-LErSEU`bk=)t!WF@tN0h?yp%wfMaqkq{)D z(>f7yADo!0`0{zo=%05ZqLrA}xOLc}2faqPJG|=qi7|-SE zFNxWR*{Gt3UPWT0xAc`F#=$`R(M427{Dp{CY&<%TB#P9OCq`<}7`~&#M!fCTgnWvM zedtimB099=wQ*4(-Vu($!n4{G5hFisg-^3k_oE`0NpiYI2tpYIClNT03mHP9dben{ zWp4oAiFouEwg|$QpngwmWY;pOppJzDv!QRs1egn(mf>TMt{;H1#UnE&sIct6n*?iN z)4xapJPs0L4tb->!)Rp2P2OiH%AAjn!VVp+fcRu$5Z}rAT3Sd6Axl|+IV*Muk<70W z2pjg?FfJ7@@ZC}^a@OKP1%XO51+7tRXykqdCSGHS1FF7mC#_GWdjwfTAm|4{$>IA# zw(;sESNkJ}PiGleUhv*z7C|oTrUM^_y$z9&1<<1*M5sm$JE2OoxEDhI!a6+lp?IeR zLzq|bTjpQJ=h#6{E}rZU-&%g*X~PglGTo@c{O}D9`Kn&?bidK@A_la6+5nYd6(=uO+?_XwW?OAbM0Vsnxe?P;EEQ}@#bp>lD zJzpMS5jB_*Uy+1g>jTQ6W0|lPoF)`d37}9MwVajx02J!|PK5#cMslh6mAmXE#?Bu7 zWZUr{5bXW%TT6FEutaxI1UAS=8O{iwnu83p<+qc`%KUa<|O>|zK<|ZT&cS${#v&5wpeI9+U#7TpXQ*o!6`A7 z$Be$PFwT$)WowNqRG;^9`{PEr_FAdetYbBV_%shMTsCl$_W0ZLTw#&4iyu$L+kc>0 z+?5;WDvz>3i?rU>1!_BVmk5~5VqsW;#u`L#gmv zAabprx}w}-x>I&J*H!)?f~xiPoB&z;8@S!g@N39cgd#9O32Cm=)TaOhuZMC2l#xKc z0=9I<4Xa}II*9F=#d425%h)D|C8f#@%rFH|h+s!&K`Klv3B?lxSNiA2>qYSxpMwjg zDIcc2c&emu6$?q5G`L&nJJPS3A9mL9j>SH<-gCbYxGENUIOI#|(RC@0m)>%;hqJejr#yFynKSKn!q#k4*y7!AZY7FT+qDWn zv)`@d`N|Ax34!6eWyiC_{WI)nwB4Vit{lTLKQ8zu(0vP{eO$231MsjkI3`!qazHvw`F4DEo{(FEpRd}<`iTVS;bIkfx`riQO z%l;X3y;xXiB79MXV&Wt#8Psz`VJom^awq&4bubyb^=pkxbR!VhjE<);gL(6o zG#{dC=1FQBsaD74&Qw)X3FKRuK9h8RWMn#H!2Z~vx5E6`%WzNNDg&*g(G=U@aE#@b zpGs`VW3qEbDK&85it&37~K@o=Bv@ER9_Eo7+ zur1Ct_kJzF_EtasW0{U@vl_WjDNb&r+>BRe=JOzw1Ut8MhA^hNo-EVKeKh8A=8D+X@Fho-J&)AxISxnl zN=f|<-%5UYFWG^bE}?X3&hX)wL0YsOv*<_Hw>d}eV>_ANKDmmAe_)IDvFvfYox;&M zlY>81t~b6>XR{wraQp#gC~@rCwkku~o#o87kf5#!#U*pGkw=`F>_o!ORv6YjHDLMd zoe`IKnv4Ngg7*2fGL7clN!fUZ$>qeu{KoH(%6`TzJ2ozk3&ts*4<`8ovYAjX(2GHp-I zW#@7Z`^;hGYNFJ04IpsBOj^4YCa3J+kQgvZF(Z^Y{wVE+n)!u&z4b6q$YATN{vu=2*Tv2lv^z4NGHeOOzX%eefwuVcyYE1-K@ zKN9#j=@R1kGSFt&YS~yi`|_U-?2jPFcs2!;A+w{@1WsDjE>gSnBef0J`lOOvlsTLt z@d>{0zInr;wN^R0crLJ(exceKZK)$4XV8HuDT_D=eP=We(%42q%?O1+ElT(MOVYez~C(1tW=mjTsS8h7I`Tgy2yVq z1_XiRnQrt7&U{rrz?>{p2x51EvJnaVC6v(Y+>QAv9;jSRv_@*#wm;%rNMWj_ zd{#0W<@r{|<1YH#;cc7!96Ae4Ey)GE= z>&=zmWLdb6S{G(FUvW)(-Uvhjr-UtlQc|h5l3&Yl~0lt z$L!6P0{ph#PTR;J3+C75(DkyXp^wUYMt*-+{O#G8ZzmhNj8;{o z??@>WR2-EiIZ+fH1G6S{kM0?2~Ly+U^Oy49zTL zB_w`$OXz0z`NKQk979k1Uat4w2gB7fG|R9bPi>@ac)Mn1U|5jB^#Ov|fDd+k z6dqFki+jUsMVXV1MWyc!0e%#)H}(RDL6IBj0@Cv`VK2r0;0>E$trj%K-uC9 zxE7)6nQkqI?i*#n2X)hJZ7+^5bZmVcOGSdLTUtz{^5jo7ClqS(dfp6$p%-2n(Z3h> zG-Ech%2k|@zbjMN@7H4TL`(*#0xE`*{e1XLFMf<=@pFFifN^(sFy^DY!J7#L4*$ED zUZJ$MVgoa%O+*2)K=e@U0U?UKXla*$ni{E_P5iBbXpte6 zagzsA+-v$BDv{D{@pk?^E`8yFvY}e>KfWlxQ~pW7gO$F05-D50mK*I*CGP1oP?I>` z5mgnHA;T?6nB6u9HQUo|+#O1nSz9SN6Wh=(l|5?wdXr&$S0R~Aa&RST`0DLJYNuvV zbXlOqO;(=P@XvR4i=rCJey?v0nF!TE-T9;%!(?=?^Cbgqk((etcQW%?QDP&)@BvZU zKMemo+1hUxp~Z2Qhf8y{#wTf;adfmM>>ad539S)4#J|0~0Gq;7W+o^wxV_FS#&tk!v*J?yBH zc+s7>RIl^f45es%y$AUrwRMi&3R;5C?un1#Lo1!jLi;Q=nXa_GBd;CF3D&c~=?||v z;(mRTq&2++5!G4#+)p%z;qh^ z^N3}GcAesNmv9p*Os_W2U6ZahFYZT#^~WyG)REF5D!yA^UD`2v(S6ie!JSHihlWC< zlra$~lCm4$_-Z0kLQF}n-*vTZVfdq)yrs@`#8jB)$o8bC*YCT^ zgg^FAR^oW^&3=hEVyL!r!;5C1w5s*GpxRI=8`lGOKgg+sfkq=L4Da5nMnh%%8CGkg zSku`2&mJ|1cn6yR!Nikt|MQG%@nd_Sm^D9S*O^~;`N0GSZ4jkqetrA()54BNauFz2Tw>B2}buc(mvf1C9BUPT<8~3oI`e1e6r5A$u*gZ2pP&#X_O$xlMB=*@ODh$X_6IA zovMht!d%OXZDumIfJXp?A_88gvO^O6!?)EXyFgkHXK(SR6bkw5754+Kw-SI%!*u=c zl3}5WjwdudqG&@QjXV`r{k1E}(n4dv!qCc<>pSV5P>2X;L3qt+G|M%ClE+Mmqk`!0mf z+S_oDKCVKOo*eF#+WY! z9Z_6`v1lxF!$=-30kGf`zyd;cL@;n~r@n{@O5Ac~u$-*Jr&pgV6EC zH3A!wNH#-}t7|MsI@Ttm- z>HOq`l~h{s2VP zT`g+l>1CU?o1`>5^AlYzkUjcA@c4+ghuu7AFdTk-&9rbz5P?zF2T%v@*169ULXa(% zYb1)FxFL#)0JRz*LrNjJ1xl~C!nJru2h}~~SLX1&oCLxvTj`%72gZWhH}QXZ0isL> zTz0;ASIrJBDUi4fJFF^tTD?njF^KRE%FC=Wid0K|vdzJ*Ud=!2z+v-gHOFKJM~|87 zeH2CH{;OU7?Mk)w;N+8c)YUn*WfY}mL8>tC^Yh#z@+O+*y~ITd&un)Y%`uJL5e>M$ z&{b);rFx-lA)`WPv3%)xu=1Sql}}!Q5ve1<_K zc=KjoN(^J7ve5mxK!N2vVbI+XLH^Zdc7vFDp$pp`gLtFLG3#-!tN`EbndG7{VPYXP zgZ9=AsCHF}kq)+ht};#}k^d`xuBL%9 z>=x~yuH!umMc{8kk82KuO3%G5-F*smX{#n;jcJl zZJjptx9eQuY;-=Ufa*`yt3`(JCvb8rWj!Ee+Mf$Mc*2ObAnsa#VU zFC4Ablp4)68yt~UD@aRp|Cka_q6Hz=AZhF{-(i}dL}CsVIS3*mbcj_wN)@#)gfFsR z-H{j_Ei>2@MVf#XOcLz%T_|mjv^Ugwg+MnwjE?kE^k=6_MU{72Peq+H2Chgk9xjpd z=YO|TF^(D}C-UdvHk~^AtY$FO*j>Np@q=({KHdPF=e{<_(1LKj70tc(wf-#H1`mDQ z6OX9DneFO}n3(Y=b16o1!;f}EUpG@^Lp!e|?i5f|>gK$LF+5{Vr)%~GJT$}Bm8u7- zjP@!NEtSx8UylWnj{uop0_+K=NMsHINl$yhJmOxS7R_ZlP(t0(3kK?Wx_&WPil<)f6DCagl1{{I*m_s;z?t9*J=RA2!4s~7@b^MU?U7S|5c+{Ta`iCPo z8o%^SJlPkDLyiHfg)j}e?+Y1YG(2ai2CX`cfs$Tt7?Npo%~lS(uM(STf(8|1jcn21 z(>8=8iMwx2@`<&kQ#5_-DkfZ*#Q`0l?_J=S=t$`>QeG-iL>;dB479c#V1MSv3nY@^ zZjh>z_IRO7|A}p2fRaf3bB)-P;D@R*O}l5_J(%ufh)m{uN6r~v?^^3F4YqXzM}hO6 zkm{pba-a>NH4ESw#XZj$zz9l+bH4yOlVB-)fIcb`cr@*iI(y!sxl`_Yg_Fj~EhhjK z3^PCtnE`eU7Z*63YdTW|s77WBub&PgDWNqSkzXpp`xC3oIQ2Z&Ij`K$5xT0za+y>Y zq&f=ar2SwTAmtqDwyAU$ibLa^vQ4^aqL9kr58vf@2A49vJ`Ym-6n4$7oGPj&7(1xm zHDM(p`-H|V%MiKNU27?vx_Uaej9%sl(GidJoC$@EXR+xqkMPC#64JAbd&x>9(iq% z<2O4SsRP=N=8-6804~SEK8yoy&l@*oS$)wLksN3x8`9nh1`_>W&@IMGugm7s?ORKb z#=OP7$_+w^{TioKjI#;O^2-ocKYw_6lyZDtv;N{qq4H2zw48J~Y$VQ>jH!lE1XPNB zhZfTqYaAE7<-gUmmJS_&bUmP`1)Y!6vA>glBjL8-)8pnPpwg3JVH19W>5A%_eY8F2 zxXrgV)g@449TiMJx;%ZQv?uT_eHstDY^^_a@D)z%jdw!u_gc$j!hp+Qz4QmD*D*Pw71R zOmB^R!gJ^Z$}ckW=z64so#Ohito34*QT#GEQ+W<)vfHx;rRJz^hr%aWULQiB zrTWWMr$RKSnmTFA@21D)wJQyfi0t@#vLA3UB*l=EW9g+9?sRbh*RieE{IY(XUWn(7 zK&Ws8OC&`&Wk&fTWyQgg2A0X;g%KqrDU#3h{PeJen}NQ&l+jS&re92Ip!ep7q9F<; zNC_EZg2J_*&;9vPxy=(Pl&$ri_K&0(u12jSE_RNA9j@t1ltK|!(~4@xFSaEI)|@>n zXAL?tF1g`IIBST+@QZGrY_Xjh#TK~;_>2tsJYvIKWekLTQzXP=$@Dyl_FDE)Rg>np zqn$V%o}Zm~C&Px;rOWK-89d6w%%DnrLJ^u+MTd*&zur@Db z)*qS4nu`i$rgv^M!3ojfyD9@L)f8}pVk)e0nE}XvolB)o8}KE(SKr6_h~s5BO+KgU zDCz>l;}45seYv1I86jSmd5`!#I zaUyq0F4a`~LISpwb;1qpLngX~>o3hCvGR0?DnH~Lc0*;_)ndkp^E3D0_<8H+Kj=i= zri;7^D6hL}`&DuW<`9~2;FUOHK5lRhoDhLEsxUXVP7mP9e(vnU;3%IE5szB@=S)%8 zX|{1sT@jh+Lm^EGuLQmpygIR}T=A!i9~F==vaZOh>A=u!v*e&WCLq93_?O4& zyzE(uDR%CYNw2PF(fGy&<|0Uk@^C7@S|FN;E%TZ40&SW&$aC%IgIwkp$X)9v3IxrH zmJgPjp^$DzdRyQg1yv6Q0q&_+P}j@ku%blA0V784_G$yOww zmX~G&-Lu5;cJBgT++sBD{AAfbF`@MAauTD;cy zOdp?j8})2s=`9+u$bQD^jGoGHi(8Al|a6oL3TvK2Xd~=N}Wt++lQb)#v5eg(0n;BW7Eg z!VOn*Xr+y4`{M$8H4Ky6mV_8&@+5~VVxMq$3ktD%#>MH=I-wcdhA_KPO3lv&qPsZI zE3a5{k6XEkOfJ5Fhv`5Sen*B=ccqf)nMMk#k2t4n-DmvhNV><=@)n#C9*Tm#2)C3V zccLz|7eFhAw=a*^({dcC-CIzl7*O!0lMIMpw05Q!C4%x}3z?IJSA8sLuiL#?(ec_#rcr{17A=YbwGESDzQt-T~U~agu0QojJ$KcE?&wUSh zLLkO7y?}sfp<97~LHMf1j^|2gx~H9#WcW78Cb#hYuxO=N+U=yw+AL}Y>dao}mXgOV zK)ur(eHhODC36tqE0N0Sx&ldB`)!85Iq<`0fm+)%kqpsSXfehI8@~`h<$Wo75@N^m zfodt1@FRDDBOIS7I#7b#G^Gm4}UoyMz^1_}U1(j2qRhjoXN`c}vtIIvr6Zc;wEsXTL9z!uZ z%^~W;YTF52QD;A@LlZ!myV=?9Q+Vq?>~QytfZ8vdIt@#y-@jDpft)0VQA zlz}zdp6NC7BYR-vIn3>EyVGjXVh0Us&wPHC0}g+T3d<06CggMU;%fghwnXG!VhmjT zepZYRI7nC4-eD;KCv*V`v+cWgyGgDNLdl%SXqcmco{RX~oO)}oqkWU;vwhpg1>Tr5*TQ|E&hxoYU6-ibwg&$fUIktgj(us)5s>*8| z!;2Hd`noKF?drLIQP8|VL!41VtCkv#f3M0VA^J@V)lj9u11hKO;w za_>X;Jp<29z=C-$Z(FLjz{(Y%9SzsUT2#DbL0a^|@#_ZkKlN;m z*9)MP(N~vD%Akd5weFKM%PF9L2#1bh)O z^NRH-puaZ$-u@5>j0?w}(%I9k->&l!gl>OvMq1(VAm?$pI**e<5n|(JO;6-w1|{v# z*HtUa6Ifle^cio}g2?kf&S-}8*E$`YKxwWre5WhV1n#O*SvW+t7qR%MTn~bYkzx*8 zo+QX+I$sG$;nAt##7W|}NYOmfAkZ8m-p-t!EZkICOp7_LEK~iiv-{~4VI-}kV=FVQ zXG@F5m&X-*i7ZKVwc|G{yKbICls&#T34}3^qsVTC9RPi$!VxAp2ZKF{+GhP;g6TS! zIR=oH`JwT^`Pdg|X^vx5v%$6?^kF1^nQT058XJNS_~?^5U zZM03?A<`QqZC>MTef7>6&e#tgT*gK6Jkm+=7K|BC)1I zc9A_tqD06bt;u6@VEP2kRcXRvp}Yav>@?g6H~kDBc&k=T+I$ps`3biR!<{OdYPXHc z5KC@$Kw?_c%bYhr15{VOV*$m#7uk@&?X((LXL44F0Z`2V0$w@$w@o(OFpxQ*nY4gr za;0b>9-?6$94yQPE6@US_}~}C;-!bQ=jlL(1{6rmKO6xR13CD^mF^zO%1lU;=<)y> zo@&$ww0_wt7i$SYpy5uxKx!bBH0ldu$~lrsC(7qSWzMT@%bEZxL6Bwkk&^F<=vrp! z)W9q3R#QMX4ZkhWhx?~e<>I4Ov7x4xS}iO{qwVR*2@UBqmLgwU{H*#_+OS)YQvE~a zn29VL#x*^Z|hO!=KCGbpOT5Lkb8Oz zF;E<|yGLo~GZVfR7L1+{x$PqZ>l)$5p#9R|`*}SRu|tzr#_`RrXthZBvW{ccfV&tx zmy~a&HrhT;tD+_|J9t<=&pDBCxHHf?yHNo2+y^dl->8Ch`2w2#TYuOEKJm?0@w-2w z^bA;K$pYxJ!X*Io-S32jA~Rr7GNf?BnC? ztUY9VL%GJeyN@nzj?oiUVX*y+o~?$uSE2pHyp_3XtMB5vNaHQ#zXhXBZg@oWWCSEr zcn&Zri=C#a<%2bpw`j!ES;TeVl?qglNG)P~d+puzx*f%BHQ!C{LT4V6AjU=we9|NBggSwiJ`_)e zJ6jBtXH@mB^a;(KK~Kfq+*+2;!Q!3wcxPA<4z%`qJ~+{SHFj_M>MC zoh8%c;eMf4$bq6N`A5TgUw)o8z98*IAM8j=!qv+;Z<=&V?-1L%FcexT9;g$Fef_JJ z@_|CTt&u&sv*+ldMYx$csbKdV!VsfcP$#BpDXp&_Sm+T0Q0N7^rHsQb2&pZM{M~1I zA#-+yCEjp_8x%+8H!w?=gU>#gYmyV3We252C|2mCTaIbMpQ4v^_NMI1hO&HizRjLT z9!<*g=^W=iBYu{6U{dwJyk#V*_b920D80EZGtgBLG2w z;V1Pwj8mBIMgZ$m?tV2;JzRqM`W#dE5xeMd#tE-cSkZT4hW~1xKlH1)Nn$27#vC5~ zd>kyHBiyn=j<)N)yD_gjrWo!aa2iG>$guGq}sp)n?%m5WPA9 zpVx{-j(ROz$wX$N4412q!d>d2fB1o2RY9IXBh~5M+!DH2mB*C-XFe%bb}`iFy>^}S z`?Eb1%}=9=3m!J36sdpF)f?^>q+pAE$Wh6hO7YS;PzI(T{j?S8cy5p_;+^pGzI~5QKDY2YBrEJ|{Bj?ie{jYF`8;AU%TAW2LGC837@*bq#nGDsTo> zfrj{4MnZ9*0YR92=VQuI%hO>fX3UsTrDSYW5>K&1lm2ok9$`h!RFGXOO7!FrcY$HE zHvdzifrVsScN;WpukEK~8m!aD{@%YR9thXPf(b`*H^&&rSD4q>tkhg}=Av0t(MI1p zLpH)+ff&R=+blS@MQ6fi9)=?7^K>lt?`sKnDI_j&Fiv)h0!7w}d!BaXVt#lDloo*` z^}Qw{|1uwv`!Uc|^t2XFt@{FS$q!nt>T+s7X84!sth0r^m)y`AGj0=f++dY*4WDyP zn{_rX5mfAx63_LzX%Z3386VJ02XvtPGmEBB&-@VatgNBu;)x+S88Pi!B4+0!9$xeJwCRPe0%XMDe~qM$YAsILZJI0y=b7EyS}hE{bbmE3p7 z3w%6>`I(5MfUb2EcV)29>cRrRtB*6%p$A5fQN9B;YS3VPH23{DmAfHW=r!l1(;+fk zw>~Q60eKVs@{EjOeoM61nA+jxtJk?(EN|5E8wBGl*CVu7@auO=6U@53cfNKRw|WJ%+oS?e9l2ju>V5 z>XKDeSjw*ZLRvg*Q>xhrsazKW7^fHL4ggx&llP*#R#lZUZ+W^a8E-xUd7=rBAG>o{ zA>D^@Ttz_btD%h$SerXr9H>|C1CV4bcq~XIARE4OwrY?kYrFWGzwUCR;9`BWqwh8K zDtiojk+aeF^3i}hZ;*3_Z*Tw`>BHBTt1%vFn-)11pX^=ZZcd-9g@%tv2pTceE8+I$ zp$qH^Xl4rzsb4eTs%mw1APlxl&>^VjQ9&X&5$G)>sNbi+T^R{fZ%vb7@J-M1*SMI|=a zNBg+-cy-lP8yBX#tf&d;$30F-xTDHqa;WDfJ|!|+8<$QvHmyu)W?uF_o&Tgo1W=C( z&#B+BTwuR<2?26=)RgB1fC60r$Ju$LxumH`UcdZHUDEkHtu}4j3jjv<7E!y@^H|b46~GJX0LuDaJG=g7#}Anc z#?i|-Qink<+#~QScR1X-6_3m=IwNzOxd@|8FsWK?10V0PVt|g`bqrLl(P#6KRB3)f zjOcQpzF)O#I)w6v4lZLudm25dCWX96>Py{M*)~lnVw)N&t0eD~?@~20i%2HA6M$U6 z6TUp-NV@*Z8jz(!y&@YCJkm8pvSpPR>^U!^p*P$$Vudpwk&YFe&|jB5qmV>D%5BM& zYSE}l8Dmb4)U{U-kwNM?*wq&;BPOIc^~3AHV}4Jn(|7=d8?uslQ%hlJ+!t#L_-h*KpG;Hln-o2 zQ$A1&3mFcmnjzb&G!PBYLOee)zO%T}quVsvKD^#C-jy+38L!8Z1%YZ2X{nZ2UzBbG z+kLkS)Z(G)ow=3Zt`!TL0nVe2t35L0&8Y4KxFwtBQeEAfFLiQAOGACM@Kp2KQjSxU zyz9J0ZIYF$P_umGmq|$>4ee(Y+6F5t@z$w$2R$ zHM}#9%nuh^UbM;6n)L{z?e{npL>Wc!OSV8cdcjIKcssT{%~#xf{9)jes%y`y_IOiN z{9i9YK9@&iX(;}5xBLe=!^wpd_NSTp+=dNf%!T7Ymf_V=)Vp!1;$ZnwQh3pip$ z1|O2te5$MnjT(N)^8wt)2pSS&nN-E2y+J%b)>cg}ZwJ@DrhoAE0Jnbw zo6qEHn5iweB~M|VbyoW{UqKaB@8vWg7&YsWN=aqTk3EmK2;C23lIp)JW!I zxq&77GvjPn5@0gOoZ-8G#xl3Pf9F}$e@2)VyYPAHJ)y9(_bn9@VYJ5u(EeX1*wAeL zl`R-%y`D{g;Wr5;(2#!7L~~_s)6p#?uJ`MMa-)Kc9olwlrFEg^tt!%)$zx8`c+9C$scua4{=M8EOs&k=7+ElDqGUU* z9-u&9uzn#b>=E@1ojpbIIP2dU;8{elmf$usYs_uz|Kz>Yz7|*2N^r)AJB%NvLrNsV zfP!X9(C9BeA(8P)>w`jH>+)i`Gz?n2Jpb$kEEGWbx}-lke;=(ytY)PN~xoF~nj)%@#X{U`EP zTPd*sHoVcaxg(6vGy~ZhqAN$m3%9eS!Pe^im?Af$H=`=)7{3qvcM}0eQa%YnO>h9T z8erV0Q-K19;}fLkg}X^y{#m3Mc1LNbHX<$v0ef0M!C1_vf>DkPu@-JnrPO{h!n$x! z&>2yzX}p>7Hy)Dz_t(up&1{(?&-7OU{>iENl0&gYZK(Ro+H`YheVVtdd91IvAL09^ z<3s?iAAyQ}D>xl?SOi!zQnI>YD>^v7&N5 zmGp9Kw{3}P_QjL*dRRJ@*ux*(Cb(156r@I}-x?PZV{SQgsMG(c;5%k)QW3D-GzJ}# zchQ&!}?%V zJ3$tL$*fsLO|&Y4O5=WNnDdI0-AyGU$)ep*D2Fdv%E8Tn5vJ?s?UMDq)k34BjJRyP zO9l_D`^z)iMI|WYpu~<^u607N$V8bMy2t;s@P8r#01b_PD~GHqtPdUyaBHfR1X#)7 zwh^!7($Pum_x4tK&t2Mor~b*Igz3PU3?*WORF$e3=O?DQ=(ILXCD5H%XKOGFjMnp{ z%MrU-(%{#D+xBV6L`*FE5wW*p8_>6xAx&C&Z{4x+^(?9JlbT9LIaz$aj6L>=1Yj>G zfcv>vXV+Z7zyt8V6+eFy!^H4bBqVI87M4*cWY)9ecnK)grpCdkI+*BJ-#-`T_#5;b z>&8ri&pN4+z@edMaU@xGBizSj1+9H?san>lNr zINm5FbbR5houA5SGdDCrARJD3@JOb9BFo+WaSp1VPnnXg+!Tf_6xSNm>u_6MI@p`$ zzM@70`#(!Ocss)nM4zw4I=P@9C)B>ke}uE7AUjSi9@3e)&`w0tIjttda-^M@Qnl&I zT#<8Y3p+{+;4wFMt_Uf}J_9RX zW*+#Br7f@zy+mF>XE24gZhJ&e3K3F&6*I|ils83qC-|LW4UMHaLI={SAHoI?IbIROU)Yc72c>!)MbsRI}VBD?cKpR($gSCUN(@<q^DQ4&Ff9c{@A$E3$KaA21|Cn{kJN9 zrbiOqF}@s9HPuIw9rY0w)>Ca?qV8yZr*{LYa(T{U(489PRNus*o#Tc}C9uaY!6pu5 zIGDT27p?#75^+Lo;j71Bym@5E;_Ce)6%U*;QEioGtz!f2GkKMsVCt3BM@jNwqCLBq zIWtDv>aChZ{rEERR$WU37UitSL^a&$??`9a2*V9Kn_pxNZIWS85{NBU5r!)P9AN!j z&C}HB^=}!J1cHw0V7xF7=tNC@!99-h;J1EKE)9wmrE<8#x#^P)yE5$|D^&};@~Epp zyp9oTQuTV~fF|t$)VOmzZ7J26nzH_EBxpfH#bvgO_(9!`!eq8L*+vzi=)~_-AkDsg z$RsSEXhBKHRw>ifbB|U;Nl-4h&LzmUSZQkb#!|Mm&70n)yOwekR_J$o<8UPP!!g2_ z9P3Q0fJYIuSgMfd?q#H2qL9Jz7{xADt^KLQi($>zKpzvw*k7knZ$66YkqaKwGRWZw z!3(qoYkW(u$#xO!${b{0*1eli;nJMf8gu_-zS>&E z_u_Fg7dc&H9SN+Y(4Fa={FQH=32qwF#IIpifFr?2LXLl*$y+&^81tic8Kv*{E4dni zWs~cF$el`YeeEpLqX6^7N5y`?qxF(doyR)OTqJ)e-C$!_H>C|jqPN0!&9)`!%iO=>x@z;W|rKSl1mA^e!_ zw!FFgEb^^u+O4LTK6;*L3WJaz3PvFdVxX2$-+x zkrJPD6=ouqJ-XQfvb=0%znE~lIu;4UlS5Gs=m&YRhqRMwZxRf)Y>km%wU=7`w8RIt z;rk*m6zF^VKEFf-lVGNdXY3#Gb7|7*{s?2p^CLkBCFHs z6lsrVf|Mm~5+Y?}?3bz8$8Hr|v`c?-Pu0NMtZ>|<#{>h`E6e1in+Tfh?SsG=*`u`V^Kb_QqlVU@)A^dCaciM7`xGt$2LC*J{ zcB&EgZ@Fbf+)HuVkRo6*kc)(Kp=Xdz;84aa^A!{1Dvr`sv=J7Ud9|k2UV~RHEP}5BZDbY?tVSzrxk44dac}|7@Fv%A9ZbrBxv$QhJIx z80(WzY{9Epl}s#d?;omJKP*aI;V>^=gdZ7A(L>-d4_Wi_kYqbu z_M3ineJbl<7FWqPKJ_$*|8?%*oZ~yy>ou7lUr7R^*vpBVZvcLpi09M)xKnH_uF_Sw zDA%8Zeyo_0s6i)O1LC`-NxH>rvMW@NwHr>@O))6jeJ|h*cMOVzdF#SbEi?a}#97?L z+@0+%{TmBH;jBGpl!PU7orw5iR7~~y^BjVk%JBCcj$%MANrdyiGT9Tb)iRxWLRR*f zo>W(u+%pexifX@2a0C?|MHbeZOeEQ?y=>uv*WLA;6=E5|NT5X=B@>S+O`zw@9A7x1 z%T1-ldgyw}RY~tkJtj#q5||<-~!9jax(wL>>Saf3a7vyl;37spx0deJ3hh=&iS{R z15T~S+^C8rJC7NfD=b zbL=q;`0kKBNnRF^(cp+-zu!CkQ{Ct2m_mi(@GX6*cKPt*^5qlx^RZi+^Zmi)S_YAa zH@q;D24=`Nc`glXM?1Y9_gdaAn?X+=XSm(me@qwa0jvXw-{-6HO z4@)MU{E+_?eSTD(VZFs>b!#DUuYE@Rdmwiu($;t=u$~pMbk^yRZKDZ$!aKbWDl(ZM zXy?-?k3}wRooNrA7H{_5`dM*o;=N*ur#TCURZAu{#RrgR?~?` z4c2sIE;BVGD>-+A@17z^_-QoEM%V|Ji2`U zaF}W!)oB{49_a9V3zL6aE;d^lcK++0lfYt&q)QM1co-n$9Dwx)T4>mSJ0p{@ znB%rzk-E}q1GydyDYFG5XG~d2ss>Ts-%xpH@o;|Klv{8>Rxyic6NQ2L&c=g5UsmUx zftdvsohOn}JZY%wO_ZmLZiP4$dwW1GzE9v=9TDPx^df6u#RjJgeM%t&;2#9DY!WNZxvB!)8If=rg8bt9guH^vtlet z-p1QKz`-s%d6O8gD9=j;(ngs!T-OR6?iU~%d&NHVHxwWP{+zi>^^n<@`eBr7Xx@N4 z7ks6TuHK^JyFL!67f||i;XlLUIsnq-k&~L}J^DK+GoT5vVMk%`gwwT>p)Rc(6f2AK zk3PJ8MAKFJ(}V%+U%vKJ6(eCz9)RZ4wT2zcHc5xH?R5D^2_O!wO%MHA=5&JFUeA>v)PN1Og#|l)f_|sFM8r#uWxEz2xxc#ICpU;3J%r zlE`{t5yFM~ixXHeCS2J2P2^$9Zh6koy<;fl#E1WuQ11f^?>e_8(9j6>C%2P zJ!^{2i*=U;=>iIC5o4d=c&^rE98lEy5Rv|6-LpIC4CDAf?wg~*2DQq<(PlYGqgqIA zaUmqlUI>X=2c@<%s;n^b01-Ab&d zo426^ME(C*H!c8i#`-`3;4~<|gc58nr*KOVJD+%DqUY!3u3zruf!uYrxnUp_J1)CS z?Zo-bgH6oWJlfo9X#>V=PBPck6CsFrxjWQQ6T~Z%oHU#2xl1xC%~eUeI4 z?662s0DY6EKLCufbeFAj2KZO;F~_iUxF*r3@s*Itj|R>SbkZ4j z-RkgtFa<|nDu>!q8Bu*ZSkgk6@>@`Uu~+$Eet7@BsLI!|^sMTrjUu@%5)Bc4JS47- zqW}1dYji#tMKx1&NPg9Pn{i0h*9n@ho*luKGW)oROa0eMK2} zKTYpvu@m4Hut@0l@MQ5zItq4r+Zb#Tqi4BU_20w{4icKpB!!>syYM`qtPe3Z9aBP= zE^&Wk+Y2*N$=d1eOj<7P&;tdhy7)r8A%}tCwY{a4Yw>^tVRd31(Wj%54gNbIPTJm; zrEGs+2OCS}S->P6lnZZduTgg##A8-+Z62TS!Pi_$R^M6q^WG0X`%EGp8LbeBs{*L` z(IUL=R1G>17l=KWKO&JZrR`Sgk(Wzz$A@vlX>?S(@j9VOCE{Ret+i zi`q@5eHwZ@O@7HV8OcKpxPCV#LOI((D}@j?bs4>oj-Lysjx!C|<}t*GumFDTqa!WA zbYmP+=Xwf0O?PZT&CkB`l5AVZPIYlI1kNjwxq;u>8=Y__HaGWrn!FRgCcM|eN0X;x zL-F}0^5i*}|DkNH?12prmxlV`$jV@Yo9kT%JAN@=KGoA|&|S;DZwtn#@k*U2JFygs zv~T|FTvT^rUEvJQ%sR^JKBNOk+h9KyMF6&7#%*xFkPu*Uq~`imt_GVkqrmA zR#T94+G@p-i{}?@J~`fA7NlTAKdD%died)GlpU5;&KP_!mn7tA%1yJDA8YIQ7FJ(V5GTXHED5wTCo5+ zg`<2bZItS68__u#VLRDR?W(NLVd1IznJxd0o;{G}5BjV@Q@TAWxOd^o>bL>_r zpxV;+fb0d|CB1S&YXi;4iM(Lp&dZ9?u{^e0d-4RF5L|<1-bD^Zg_X9UbgQMgn4Dtc zM5WRpGIX$eH>t=leiA+C6^Yv_0@TetkKGYWgkuu?SVm6XLBAG@6Ajf(kooZS4-<{~ z7KuS*wXu#4XMsmRVATosPCs+J;BEMNJG?$b7M?uS9gAKi19hs7_Y2w$Jd+!$ zHbY^-U!#nsFSjBBnz6(?R1z4+jl%Ix%gD7$O8ibR=O?E>-(rg6uwqiiMmP3d?K-ApFMG%D4Cm(A*iKTTr^J3nSs=${cJOgM!RVQef0r+B!MxW-exKkkDgf=(1) zE@4{;6N?s}&Q*Td`+3NmIXR7vM!t#~VCJyQD3jJGT)l({;t0SDM~=jArcM!Cz?kKlgHrop#i#H3ta{vP!(XtNQcs_Jp~BHF3gH86HCLIG+Ir8c$6J{c zHhY2mI%SfLAeo$BY3-n{mCDyt@7}`cJMz)A6`a>lB`%4aj(DA}EMM}$rnCS>-Q}>L zfE*jL&T02y5XQPoo=Gx^G#j@AW1gna7ewn>!AN7_W+r7!HdL-a9v=oioK88nKR3BA z`5#efg(P_gj7^R6?+!xb1qDilg4yG~Sq^n}zHY4F@4OlrEV9)`5;4NMt8Oj=cA@2N z24b0%0T54DDO|jUXM`mq+0gF)dI7j3xJC_85lxOUe?&(o2<^;R3}74r&8i2xJ4wzW zo_kdWOZ;5o*W}NB4Ur)u@zvuqavT_?-c@p5BZeZR%C+W&DvJbD1P0FDn>MzMJ<=m~ z@kaHS;NaYwCpcUbYL%ffVF3l=LXz70sUHnQ?doQ?8F`r&9s@L1*$Buz2L30I7XbM_ z`--|&7stVzB7}Pnx-MR1m%6}iR4IQ#F6HA}253P9D?U9##`NgXgQm%jCVfjw#B3 z*qRSWG|=f}6FoszL(Sz{o*M6RQl_Rz7cq-DK-F-6TB{Q3h;crVX$Dt2tAAyC8HAR< z-FjstaL0U;+hwAj=MU?-t65<&p|`5%9ERUiJr8^W*>Z)`k9s@ zH1%NEYeOA!vEJGw&t?oH;V>14jl!yq%gE~IL8g8)GP~L94Y1~RTV$l6XDda%{giio z#0|msMi~)C83`(ak3^`E3o`--LZ5Tqu}Zp&Z{iD(w_T=(+qeIM8cgcm*kFhuuF{ez z=4Baqnl?eRgj~0>|8P2rqe68c6KIiA&bMc)HUT&9Pf{9&(=VPyU9kpe3akb=3d1Ps zIVVYrjOeYt3mvm2Uv0A3>46I%k^>W{KJMh70G7xB$5jajo(v_`ji8~t7wV(^KQ!oO-}VKuJLn7Y?crC zqisW#f*pIlB}PKc9HXdi_9o;hw%LLLPwNUaEXDEP&x#Ly$j|)x<5e>%6>1Uw!Ns{z!>H9cn11Ic3M&_*w$}r4Z?hzA(<8MJk-U=x zj*yEz6o0((z;G;QdOJr?p0z%{Lfl@Z{i`TaW>=*r|NT4>2%d=_w&A5*CS?A$6f3bZ zK%linb?Gc?wCeALRYJ<$C+PidYO@-f%x|p_nCpM0mdS69az#?NmL?lYyg3elgJRMi zY4Ir>Sk>nf=X8xyVZK}|ik$|-4K;ah?u2?x!R~>js{aqScTIP>lk1*;M~7V0;RmfR zDs3qI$o^DHftYf5v));8>C-OtptCX*9DNEN!>!Fv?&h^8I36xwK3jidwP9P-*rU*H zWqUd9Bs{LwfAV4fa75W`O}Uw58HPUf72dmxD7{Ny3??!sPy3oHlBV_{9>1c8W5AV| zSn##wO+|4o*Cca9@zaM_#pwOh^-DFMh>u^BBChP2fuObhqh#n4DIjw z8b@MN515Rucc28%_6K41a>?mHSkNHrO(EJbz=e%)K)>rc`A+a|0-iEtyXp|%rcq(rv>ho2+Re0A@|4_+k^cT2n#=k_{`b+oH)7>(6A zm^d=5PNUjFk3?kAv+}O{6G{xR6v&3&GykztHm6MBe?KF0-Tg=b8K;ZDCKEH^5Qt@I zU(}1qC;qR7ZjA*$g+IKQi3f+b3q)}!?+cyjcK@B%^fM!C>?)+YjsdR3qyc)E#%Y?P z_=CqsuU`0^o-8;O@_;a~kZidLW3K4*6W z7YR650rP-Pl!nr53nP&|pl;S%28XfSb=(VL(|Av&+oBkIC_6z{SDwK?Eu<sASP z;mt&K^H}XklSAApCvO?_K-uCXPy>#EH!5iMV>EjeM)PFQt01JM zJ2`p<1sc=90JfLh^Eu72xtf4w|0G_GGLLzs2NU15c$J5NReh#ZoEYg47$vy3KMmM1 ziN@_$5_MLjZxwb1IP29plY_sue(r2yWRV)RqofyTn@aGZk9Eznujmu=sSYZi{zr!~ z#E&5s0FVZ%Z5IhC(Nr!BI+U)v7Z@tYGknoD2ctG>ppA#sTu|P+1tBX;zLip_-k`Mf zI9>Ign@PvMSBG6UD}nP~Ip?w0tovr+ zeMcy$7hj;6I55mJkNhVKF&*1BOw+=^GaFn1IsDpZ=$5-U!=i!TtEoavvPFgGe8yNlnev z(sIxS<3i?S_6{GY>kA+oWmkr>;Sa;B3fi;4iF)vJY*>|OSE(QQM^{Uv*k4Nd7F*A1 zx%kzkYVtZ$3eY!%p+B%Z83vEFft=zNy|*vJPP#jrT}9ac^{1rLzdWJvKp#O9Wnz^Oio?K4;U)~7`lB&1s9C`%V>$V_JA9Dp{Tb=1`4*gUSW`dnG zfMJT~E=Nnf46G|LjQEEXzjV(xFC;G8toQQEi9n^d0evd9d zR_A^K$Qx8G$m^r-0mvIb>zXl{mc~w_z(D~6Mlg8=rsJ*@rOwc+Cvw^`FibVOZxiON2kbNyl4K#kI#B&a~pWO zc9Z$X<f z8W4xxl77l(&uy;R64}}KvZ1VR(ir9OjCrXTF56V;3%ptdzIky}z<#-iK^caTw88F>f!8FPAUm7u=H``Z zyW`qY*1|IT&X3M`eHDrqIaF1Y5>!_TpTHUkAGKMpAwnX*A;>%4wAK)jJezd^~v0Jst`UKEYIN zHRZ`<8OX<{f)R!#yj{-ul*wZxHlN4a(GVwA_pA+pDI@CsmrM$%B$e)opSaeeX_eMVNtef(nijp+r9Dv z8M5(9*M3`WzN?Rik5b8XyT45A=M1wbgT~g=DoJMzK8PFE-Knp@uT5y9)H^!rL?1Je zT)H7`2n7Rizf?|xw zb?cQICX!LaTQw<$-ovX2MZi6L(_fj`4;#U^*#5z1OS0KWhMU>JbZSzpEb7Sir-^u5o@Z1HdL2oWCTJRd}EG{7tWAb^nnx<-Wo|3R~yD z>g$V>iw4;Uyh)n`KE`NM-x(g78Og5x1pD`K1w!wm!Z0qpKkVVO%NNo-Q4FtVg?{C3 z`&~Tfa&h=0lKi6TFN7dBHL1{|F8CG$qrqmN;Ss<3lRsnjchZ zZ~g`xv-!)uG`-)?H2u_4rF3%XyExLD1vs{UTH&>R)rAyzT2V`J@%&mJ54@$vZz}%N zJ&XoPFc@9Mcr2yd(22z!Te`&BZNK8@`|7RdR6mANr<-&g*jNG_V9lOmA z!7G?q0Z|d_KU-3pO6uXo!8sE*o~b+TpxNpz`Cbfh^N;Ax53Z<2%A3_gZ5xCyG;mj3F2IB*UI%T6CO`Xo#eIk1a}Jc|Q%&vH zsJ<-UX+#J%w5Bvox?#B{Ts?1htm#xE&5NFqmc)m{yaKzm1nKRiFh&%K$2yU7mWhk={l(Zl&p3meR4Sz;+VL!{hpmBY(`Q7f{Ko;Q#ye{PY zxYrx;M(TPSCgwBW-2C?)JC(RE1RJ;-zqy5vVf@z&F+tGwGx=^Rw(rMprl+cG!`4v} z8JG3}U23A~Rw_qiw=~QDDnQX1c`x^eog7AY1%eydg6;=1;4i$chDK5hh3Z9P^{HUb zAd`MkzQVxCCV@6iFjDj82~X~u|N3b;C&Ik{Bs4F0?|`NNk z{)8-l%~OKq5_K-8(~2g*D#c8`)zp?x_GAPzC>r~Y%&}8_zDq#Z657X5ZpsI`jo3x&JhaO|x?h(A1(=(;K!i@wz1accQY>UZT(qjJC zt(uXf(1mXn;TCEFI4*;sZlEJ6xP781T%u70S*^-2^<z-HMT0SYCkIMB@ z$4|-RQ2de$TcKn)mUb66ha7C$CQ6xZ_|IpYOB)wZKi^!BRku@39h1-lMc?WH5st2d^;S-nm9i5+t^qQbTjS`d6J7_Ahpz6mBM_ zoS3RZ_1zJSLb?RY#AMYHo_W(xD$J76FTy{rfBl;9BllL3JN!TZE~*wv&iFY@c5$BJ zaiDim&vo)hc7EI5`SGf!NLBdI6mznYw6^)cW(sNGPJqBFV;F6!gWyBbE#s(r%+Dxh zy6y$w`lnJ|@R|5sX?gHF!`&@~yh?lTHENw)V?7Li2SKR3toUMo6-F*s(%egMhmGEC zd-hvy`YoM$`Su<6%Lj~tv>akQ^pjRiCFyeK1kDK?d)qQO`Xp?p2Vbp_2z^} zo1P>cn-?sOE=orWkZ_}cYK8y4)tdS5#{pG3biJo_`8x9vqI1p7|49%6>O`_I-6K5$ z2K|?@;@#&3iNmqn1~}|=N9ST`R;Fe`ysRoW5;@2na@+%QkI<)VZ3UM}4##8-qGW44 zfFW9B{{D+Aey{8B;=!+5&A#`ruAO(!F!%p?JkR?Fotb4m=S3Kd+#0P!IhvH@h;aDd zh!FYWoikAyi{5dNt!FmP$BnQ2lKEo3b=0;wzob1x`}^b1A{9drL9Q%|D+|$o7I`I= zoO-YaXc!nPFJngpA}RONf#79~`=4Vj=F2WrS&Ob|C*56!^lT>%mJ#*zQ63kGe~)z~ zR@wqY1jTVOmPUf3Z18JXB6}yryly5#9Xfp$5^uUU{3a(LZuNJo?^lnvtm}pvlP2FC zYH#YnuHp^_o{RkMw-7mwg-$_gX^axf3_W`Y52E+xLoZsjlb6NPU)KqtnH#;HSk$X$ z$G(CeHYd7UT&E*{h#DGAXKi;-!beoJzRn6`o3kdsFfXBiC_qgMr`ZSLLkjP0xL^C$04os~M%8VKmR+g@r$pfDyEuHr`>v&H5ad4Y!#YF@L) zObz5q&0xQrPZWZ3RpSrNjmN{C0??0Fx(;XY{p&#Jaw9rMXnPPKvT%;UYEI(#tQAe5gu zP@tpSql%q(EgzPaQ*m#iByE%PnJT-ENvY{s{3~A-KA*)`axU7&7u}5*r*(3MtJWk{ z7|aFdEfzmsQmlA4kO}zu-cD_Z-~3ii=Nz#9_j*qTD<>RJ!tkx8XMzPk#n$0eo1k+CK*`%Rb~`#J`O>0P$SYe@*1c17VmINdAbQ z_~j}nWMG&i-1qqiZDFZMJw}|UiiEgkajxEe0}5^YFd$L?nsU(m#!1~FoW%zch;dr|E9nmIs(F^jDOsKWc^x6ao0^mKZ6N!_Sq(dH@Dr%A*j^?i-D?{Y?V(_NW-h3 z0Luh7d;5dF(d?E()kxr$S`+z1E|(Re6>rjF_OW{6FVrhyt#(J@z8mjTdEZ7j<{~+A zG>FcZAF79{Rf7ThxHfa@#jf~Z!P=Mk@AkHgRY*+a=JhV$J9$k6Pcn%oKdQc5wrwo% z`h7fi6{`7U7Gr0Mhw|TX+SdAaL2~e6mq56tqb73F+#1}Jly?lz%h>X+b#Rk3`2mZ2 z*K2^1f5qm=XdR+d<}a-LsWUHS)yjiZ^4ZW6C&Re%Rabfs|Ra1F#I-PyYmx`oMw2VS#giDAsV zVc%^0w_P>5U1oLAjg zE&daTqNqDPCVDn~mDUR~(&zmWccbPLfni$at)QkIkW|mEMtMc9}Rr{zf z%ghofCAVeBnE#*WuwhZGsSc>KuAi-Wg_xo=GFBsBpmyjV=rn$QKKIofb>L{UjDc?Ltb4;Q_h49^ z$Cv1?A8CoN#!DMx4I3r-BK3pgX!6;wee=7WmF1^Ivv(yK&yMYvS<2|K8iyL8Y2@8w zg2vvncPgED)cc9*cvR&fC!tAM{Dt4GdSe(nyf{ zveob`jT#}r++|OGk@>a(hr9Oi?Y0;l+dQ3Z%4lEaIS`wB~w}j`t7pqNHN)hC5)dpY6-Io?Vi1%mlL`dLajy4 z?>};^qlOx4)Tzgj%~u%~?NRu(aE)+9*4M2NyZ6`$`Jx%v@DakBBPRMjGYn-pI{!>m zR`5O~M}#tazAtEoV%*`8?YOoNMWrgVqSEVSAvZ0$!#vGdfy>nAN+sOV?$fFZEmF6P zM0iqPee6Ko!BJDrtq=8vc!aH(+&vq9d%74`Ogd{&N$FXPUhCeO;#zj5P;YicfY*50 znO&<9*IR5p%zwgv@BY#Gp$mLYV{3mHmQ)KCPM@O|%o^h^H(rl65FJMcJlyip1s>zhH0v zmSp8nrv6A9MbVkiE9(&d=AmW04Q0;mni+@VKb5l}@D-BCgW^$8Z$gXtMnNhpC2Ti* z%P7L*bkm~U5aV6?ZosGPd}w3=wiR{PzCJ4f>P2E?oFtWAq8)-47WLrEt+n34t`F5= zp8Q60;h5lxg&7BO^4K+jN7j!n^#&g7;c!sj^_39y(P-PJRx^rd_GaH=jvy7kmG<&3 zpTjnL*6_tIG8!Sf9k9T_s|A4mlch+1^VgpF9!osrh4d2hj|gGV0}L~#x!dycZr$VS z1rkfUB%xwzF-0JE9|oT|5|Y?Mk5H$#akwfNf3IcnyZh;<1LG1n2KvmuqKbiYoq~=$ zN+jCGiL?YXW-d{iY$YoCZmX;1rt$dViL~~#%D?3~vNsDwwN2NIc8A2E&o>biua6^X z6_lUPHv6`+gUj!ZVQVU^jb$E1dLpO(FdBMUcDFE!wWhjHVv$7rUoXIG46V7%y zGUj#e98BY-{j2Zq{Wgu7kYZ}NaM=0G=U(fiCdU>UlFDE>o?JQJedrt0-r0Ut$9Cm| zeminB`ncPsro+u^HfZI1W<(+MZ+q;JYb9cjJ3;{`NVvpZx4;WLAmh3|Pkt>v3s8cw za4W&~hfZJNt7OQQs4d#vRJjHjA#A8U(F{){RjlNCgq0$xuUwVwkaaZS zuSN|sgHZqbx-ika=OQn0>qx`ev$D6H&65N+L`TO8N0A?~`76o4sC;R$PqtrEZ%bCqc|1NE`nk6hBf24cv(Nabn@&?O{E1FRDX-LEghhz)3_G04)hZ+9 z!9-r~)?{!!bEDo-OpCcrQb_mlDS08V-Ghwbf|?*R5nu`d@X2)aVyqPG^!4-c`4j8{ z#0Fdn|FDK@(-i5=07M*RG0k0_SW{E*;H_s&c_wa3{&HUL^;18J7MF>PQpY3q3$!%y z*DT1PvD7qK=L1?ZNO!@WHmr1YBnUSoabCawoih z210{5i4epSLr_Dz_Ymp!toPF@pNbmCJjYj9fKIJ2#j(u%lh;=V5&4cidqtMxUs70o z<*aY?$`zEb3@!!O#CBPdt?74m2n?NnNQY`V{zzr~C`#z(3wI6u+qGgth@Y|6&@6WF z<3?5l_cOpu6MWTO!)l|awSLRO#y{cLIeD5XD@n2xd= z$c!Aa`>teRSu?s*PRT@BTBj(F?)&23Y)(wV>VvCWKej%^u!+v-$2e^M`CPSg$1yyE z|FkpruOcIx1K)Idx@3)g4y};65M+!)U_6mtSVuH)tch81Er#_skavG>|cGMT)X-1)bY+43qjEwB1#Pkk4I?>&+;}TIe#uvEZJ> zaV^;d&DR|rSA#~j0%$!QR~&N#3?q=v^5x&cuXHg4hFYJC&kI|`eJ9OS+bmx$?06!2 z7jnDo1<#xD-L^(waep0!dF9jp$=uUhd@^k2y>bks5s8t!*TDBuA7CpjIj#SjeLW}i zYG-n@cNSLSSs~diPm6oeiR@pHwhfslZKTD<;lDH+t9lXTvP$+MgvS&|xD|VD(@eSK z`2JfLXqGzt*^TXscwSQMgl#afBq6)~Df0id_vP_aZsEUs+s0xeg<_K-DpO=0HkoBg zNo0)7GGv~KqR2dyv5;A&3?X(Ii-^d~j*85gXZP8ibMF1!zwf`d&*$_x?RTwbJ>&Oz z)_T`kZ@o3FlCmOveYg34ptYtrM|Jvy<*#9A`DXZrbbe0F^uJfTuEE>u-qLOQ+s)f* ziZkBs%;RW`w4s%Q@9%vjj^2lufzp#q5(Nu^@&*|gO)Zs=B{XWCol#`d{%6J&jAkyfY?beN3-F*w+@o#w=p6cCJ4}EkGE;~DwH`r8Hx2R+eCsuX?pDdgF@!I|{{o>JpIrcg#9i*tX&edlg{;AGz$KJ#w)=yR6TEYFj3 zEzE>~uZGQXCB*PGQp0pPRh8Qc#LWRo-L2uRYQGsD$w*#5cl~ibOjXG4o5z1T|5jV_L)SuZJlKg+~DJYtTSl{_}nP{z@CIq_7|ed=CG z+|Gve8)j|aeTKoKE#8|{@vKC{%3%e#r!9G6TE#`uo#!<#)u|Xr+%fk#FW~-c@zu}D z^Bio|l6zwh){GhM73GXR7jyS7_xk)$o71RZYJrJWWORS2wLM}sf?76t?aGsPSNc}1 zZExA!VEbTnzRx#dDCHc+EnQ0|!>BZ!Y zkwhB9cG&BLrIbFkZ8Dc|EA8Z*%J0S1@)*S{t8^u6gM}BGpQO174@z1LTVqSPU4PB# z4OKkAvDni}8R+P8xRe#&crM}en#RZ@`bqy%T|(J#sLI^-&08$1%HnT3XthT?P%HcG zfp-q(#v98HzZn!VUGz1sUP4n_hi2YrmpU`-d9wRH+2})Yv<~Shhm+Zo8jb zv6p)}A}TcA`^IyQyFA(suZzA4O&9fG?TRjM@d;k6DqT>=J_!x=)~uQ;$d&p2%-?Zy z=cnJeM25Kc;z-Z8=lm^YjOs|DFPQaGuGF+0Q>s*U(a!?G{`Db^NgH(L8AErP`?vhK zY_=bpaq*sv3!^;DM#h8aSsrplq zC4Xc5NR3PWwfzXaPUlVB64)H}P}7>1mn$U2xqEe%pQtTytZeGf7Ep;)FU_~SKXzwx zJ#Izd1of{qRc5(&DBIVS7j7rjy>iVK3g>1W-CcO~>dPgLPnF^U-NKDa^z@~#Z)>w% zm9IaazPj@E!n74x>Bz>^M%d!!!;Pb6@`}M%^SN;MHA^nfL>=2maE4Lnvl&(;tq&r4 z!oSGA`d^+)v$qn=w#?G>2xyBG>khbo?;@lBYMU|*d+@2lJH+vqc{*OT4XjtyO-9!l zrVZ1upS4UeG;YbxZ@x8Z8*s=rk-ND>fO-t1c_fi?s=!q(RM7O{>D))Wi?ez%C z*tM$On5oboDpEW3;rc^dTv2?lPqJ|i4oK9y-$Q4QvyGlVVNxmaLariqf8E&wWfcbs&i<(Y;b#S zabrSRTH9+t#9oDegeytZLf|i!Eq~mkFre6Xs{F?5?MR}B5?Dg-`q0Z$1_PEiQZJA( zqxU6Usf+gI-N11AR) znNE%zaIj1HYo5-X`g9=t;i9|w<@$Zrph0BM1Xx(MwAxB|c&*R<)^{%8swwgBB!9a+%FMgxoe=zM z%6{$_$HRwb`^ZiDX>OHpoN~Yg67~51^c-y5zByPtAoqe%g8t!OKW>zo)kbQQHjclM zQBP10GOiCfJe}eITk?kQG$%~L_FQUI^dIMxo;*jz>>8_+A~fu5-_s<+1YQaGq%p5y zu{J92cuZIxH~&1b7mgXM>(9vQt`k-CsLhXx!NyBI@9lWYapryWCy}rS)yQ(xvZ(mb zq6lu7dIy>wS*q6VtkBDiL|)@V#etNS&%)Ti?&I6E0WZ4t zIB*1_QMMAB?5SZlhq;@hvPM+H$8)~y-udQzG3^{fo`e!wU+BTu)ptVL>^^>rvz{&i zb}{7~TDJ&BUh3iyf<>#)i<tlfsJmj+vAd}oJb)Z`U&_z%jxM5ZK%jKk>}0`=Y|-HoQH`6&GQxDl81;qRZfN)M*3 z#>v^vnDdD*fg5(iT;uEBtzg>93tRmHLJeK?zn)#!9wL`MFO@=tx#A{*Q^3nZHtiZh8 zu-aEO&Yb+_)qA+3T0%Zj_wsOce$YdjE);p+b%*X) zR)y&O1m=sOVMUqkF~^2JNvG57T-pmgDbFQ7ulDvD|1xuib^l;GE%~<$zvp8%o|_Ga zmoCVhx)%Fo#nQLpY;`rc9}a)LeT;ih zxb~*2bl}_f&Am9s=(n?7O7%@2Op;6XHsCMc?2s_j(GY*ZT@!9&AvF52EYfvU|ceU*!aJODJ#TbcFkd~f zyn8RQK%oE5d^+1G-}q^6e({1{8=+Smw+&xKq>ny`VNaBbzOw)3Ljct^8Lw$N>NKzyB<3srtr^ZoeCxzE|qQ*v~u^T>KT+)3f`j<81Ok?Dw zZ5tKaFzdFcY_gfJt$Tcm<}J4{Rc@8<^NkxdEZ@KVa0NZsl_U1DmLV3m`zqvX@C{b? z-sfTirsdni^A+x5NT$EGDh6T_QDza!C|WP34+{p|bKch&Igp2$yBbc-_NmSj@as+)=hw38FA zm=zX$eBfI8*QJq`Y+Cal20}!{6^Ru=NAO-^23d-nK#vs}6e+V%`9DCB`v+T>o$y4 z`~v5{YM(Va_G-<~yb$eKLeG-v8Sa)?lqF!6QOc9)IUCS=P?K0^=WVuYH8jmd+iNSS z-@tD49DT~+S$UxtPqn^a=R+U%JZZm-{&#NMNS z%-iBsoZjGiYnehfaw@kA>8QDm$s4E|V*}nKheVfFwc`{OBl$K(Mx5AO zhL3snhiAk*vQ!ydv*{>d!9JKyP%&}NZ9Fda{wu%fmXsN$Y>8*@wU1fnPO_KR>4{|> zhco&dXU-0imwz?R<^P#el%`*P;?%w5V$EI0UhOGsBl7-wjsA0r3J#$;G?h92(hLhC zO+2vNU z?1tzgg25de1g$=`%-pA3OC9E3qlo4J` zDvPSULp6}=f6KqqJdHNv815KIu1b4Nqn><iO;6WwVzDj*woSu1FXeepaLQ5c zFvqF~tAoKpJ|^8X(mV|Fq`l8K+_D;Nt5g(=_s6N6Dcsw9GeDa*_5%XPnx-KxvVq<-Xl}BXTB=XH)FYX_{C89 zg<_H6@TV+Hr#~A_mfOXz+F!|yb@Rmhg=r<9hda9*V^dr{E>&@q`))>v+LV~ztM_J} zanr|CF6CIN9|yHaxHG!|a{{nNfCP)ZYe-ZCYkj+q=3YY>RwFkNhmuEIEmnLZ7ad9Y zk^H3m-wrYI;e6jgKEI$a>zK!});?ZdD<7}Z3L5%o7)z?3^x?yL{*e&*na9zYc<#1f zEaP_7mJ$BD5jBI%LPKSOi3qzur7Evztb(TV^)PFBxr@ z6;@$5+Jhy|`+sXRgN$^!fMe&(un1PO^v6C`{J~ z7!4D#kivF>ZZ8(p!oqh!oavBjv#M*Xg=u^MooG!2eAx4XX_VBv86W%^>dA9-OoP(f zW0e8e<)?HU?~^k;`Zv8s#96-k^IyFmOC;U3x4rDlB~*AppXT_Nm=qCiXq42Pu?AWCVCX0 z%q#ku%&}{xjGV2dV>OkbxFIXdEX2Mr&5^~Bp{-dU6I?>%3rl!dJjI6f9fr04PN zRO^QNS3helxsE8e7h&sG()HWK2-C5>d`s-8nQQy$o~Ud4qsXCN-x`jx>=2Aqxa=0o z@^^Z&rk-7+9jCT6-{dVN>L#y4F|Aw5!GB+`#(bPfSWx9s_P9Esz$^TrY4jX_D2+Vu zWG+`^M)e#jWbRM2r`Z1fmUObDURH_xpMU%!qoDcc|60DpAZVcrw4tQ>|38B6{$CX` zlxvFuZH9Bf`Dtxblx_Yl0Zho43JqcQ0@u305ZCjl8Y*7a-9nr=D<6LD;q|h=76PYN zAUr1gLlCZ9`hWWI4eiUno^S#}jj$`onHAr-(?q|Lx<7e@I3l5}3u$!f&?qPV*ZTUQ z@KX6+@C>Tw(~*qNWr2fbIM|_tkHA?2?7uq$Fc=myj$_tva(^!sek;eQ8be+h%L)RKy)^3kzg*g|0u`>?~FS%~^41sf8A zT%Np~5ipkwk&KEq4w_;`_yHm2L3__Jq6C2?8qf}5NDDYpEkOxis+=Lxn+~E23QQ!Y zH4GvCX9z~ZMrLF#(A*l3^Rm`P#3?F6sB(91o zU1>3j{U>Y~FM~Qng_#b>_fM^SfEPm35C0qC&q;hwiYWWc3phRhKYlIFrMe_-xc4CZ zE!-~s*VX~j?`%<&)I==fa9rH)UyCrxvh=+{Qe<02Gv4{v7uurtMDNLcfOE%kk<|Yw zN3nF=fOIU+Rthq7ixtPeBu#4fC(oA`+oD>w{~JXa)(T{iP4`BkLYe+2h3WRvRJhVS zm%!zJTHXE=kxQkz?G2YF{JE0~PyQOh_1ASgY;K8lMLGL_Ri+Ff?94R#a&4mqUKa0( z=x#Nz|0j?UtZkqTOYZ|MtbXlqE~S#X=UYMe~4EQjHRAJn*X z+#-*t7T+uBo8U7M2>^bhz#!4e<|B<-^tQf&dxW4eElv6PXBp3=8Mh@3&TrqkJD+{T zxUF}%RvL6f)F>$<=-sj2lC#?g(c6lv?>Ic9N;M2_Ue&$C_KR?hox}5tXJcqD%!-}a zpk~@!hhFgzvQ~EGG`{^ST|75YUh$B)NadI6V=(^lE1qAPDq)A<$!$^RWi~y3MVPZ3 z(#+FEkUu4gD8K7tzAA{b4&Exa{UgW}jnE=$l#&sA+vzvL1s*{e56s`dKg97VqnNzc^Y*D1&yE{C=5$mOFZPr5@Af5CJSTvuymdTt3J94jIoq)AFt$H8Vt$+yRe%@3v@3VHZfh~!e+MGBJcv0S zVYn7}T*AOOTr*4JtU0qjYX4`<{8)yAR>vncxGai*O!*Mu9GZ<5-gz(QYRI}%GM@R} zRM25WU&m;FC@xRTb7wl|*e>H4BFYW>7i+siI1>Nz*cR`0+Vz3>Bz-28&lXAMR$UQl#8{l8f zCiMD$3F$yUE2lF-MA7f2(EWoTo&PK9FbzAFhtTgN!sWeOzQiOFu47HQuKoq z(>StV!&K2JM{*d-o%e4q&9cwD?SaSkM=wzc?$^HbX$`LcllOq}WQihuLhneK9e|p; zo;Nvvqw~J1WUJyW1Dbkdwf)8}yi6{!X$5d_MDsB(-B5qQpi1j2B4lS~kG3E%6mqWSe7SL6U^j7)kCX(`AFvb2w0 z;W565`8NiC6Qkl|c*XbT=)Ct2i;dw0uISIpUf7JuSRNRW;|O5c}naFqu9hj@h$7jR%YCso6~Slw?(Bp zJ4jnpcZ)mttioj>T};IUyKsc)+2Jk zB|wpLS2ChDv?B+xY}{JlbOVG466A&U&A}12qU}vl+DLm&Z0IvqFmK!)ShJ8?^_SPO zmR9T#s*qqi!6qNBqz%UzXr6C-nSHJ1G$L;(gasY~8D4rq7Ypbjf_tnvDo939!GbWH zxs>NU348vqxS8NEv3u$tZUsFC#8m-~%AY|7@le}vA38LDx+BhxA46d zX8odbJ`ZW8HA=^Ak=DpCLQAKPmUeg-p%~s82^L?4`287tbi4tM!knYWtOO7=AIb5~ zqaDeOEVUU)3bln};`4vo6H2kj#k3eY0y6!>FUJujGx#h;O}wV~5$sVTFFy&oV7De{ z{FXzSBwD`{IHos{Oi~P|G3SM{(upfVJo-c+k;xclUBDiMOM>qX$m@Djtq5o`fwpf- zCefJpLzS*VIF=JdOx;i?HJdp|u*^BAxKul4Lic41QgF3h6yB473~;>g6DjtpsJ_tEh7zgSQn>VqjS$W9T@Rkq z-M=`^CeJ*a76Z8f2qM4`0XSu|)Li&DrmV9cfuJtH?9`Gs-!N;D%S(uoof-mA=#Kzh zZq!Vil8wsa%R!EwUs%IV8nV%!1HxleJ83aB@qAyU(1B&Q<+NSpLuoX7p*AMvC)Ohv zBQHqg1gpGo1ZXz(3iyL@WKUYmB76n`qfRd9K=9`v0mRllIKF*6_!g7_^}t;CD}o_X z9|Ooz>t7tLeTsf2k`BA$;4GYYA}rk_DvIbCh8{hu!p9sNHX9g6-d<~hwj<6t1PYq!pg_jMINrgb3!Bqm_5Ipr);(_g^ z)6zjOtC;@LdESMxfr3a-ngbgE&H;s2GY}M&3+rWIL*zlFR0lYsWeX0|vJJC~Y9tm7 z_t+Vp2tY9timG~Bg_iw3w~NLQ6ffV;IROUNi0U6T2%^#PG}+zr!CRsb4&t^@sMQjr zkA%?7BPoW8>ZDC3J4MwCGinmNae72TfP$9NRLq6NFhLgz%=k%0nWpdlupA88 z51{O+&K^5lo3(N@TECUAHn)nKp1gy2>n;}?@jyP9ok1yqQHRDLQLdrPj_&;_+Xp(a zCvKhOd;I5-%PU>Q*z4CU9G+f@EHOA0{-lV=NFH2F_O4f;`xc`^P{pZQLA(cAtNNiE!-| zx6bfrBBv)&F>f}R<#Imy$_&s|q)ed>JZ3ha-PBLDItFKSnF}%erBz0gJ7n+jBcNFs`sf4NC7DJ!yJ4yMD`-koeia=XO z^0YY)pH~krX!ofQ&7cgGY9hDM&Nh1VommQ;B~oNGwpVYzTUsSu3Qe1E@{EdziV>Fa zj&r2)!+0se@H{~?R+cWlZs9pQE{NFNWnz5JYybD;w?N{kN?p4KAA zh3zhO+h=ibKGn|1i3sjNkbz*eRO07(B3=OpZJkKPCtJ#ROfRkd_k@7@wYFJuwY6mK z-|U49yO=XDz82owz`%$e!*06|dNnIC@sw~D`aDcw!0KfU!eJ#9`|6k|S@RGoDnOXb z#@f>53NZT-n6-QHQi^SPONQ-w_5sJW8dlC?gh@(b>suI?s~xFW`1oad%njSNbejAL zy1po|vBxT>Oq356y?@idz)uv^c2^=ubz_aIa9UsU&&PoJN(;BpoVOq2*HsG{vc7$O zY+#Uf8pCG03XG+k27{A%@qDhOt2n)q2$H;*rBSm_;{JU+DDd%;{===imM-_9`)>kc zHa6Tp4umbZ?~38Q!H@I=wEL`)nXdu@w*nL9x=V9NvK#}+)y~*#)NNJ!jEh~PVsmez zt6*`~YZ@OG28j>AoYX6d1x z2981EhZ$n4vuvrtT`@2iJAq-p5QPfwmxdDb1g4y{UYShwxIsovD(Xn>AVA>o7{Ps* zz|mXpBVx~35wz3GSM)uT=XwmdB55PBv>?!Ws@vQykPVI2m`Dnqdk~wP!LSuzK}4Eu z>9RoJYcxN~*q9E(UcdsHc=WqQO6r=-6|17zq=8xqNx9oLtDMG^e#g8#ps7?aAzr*e zhFvhonViWb>65iILy~otB95p4Ak4?<9mh|ErDUcDCuDh%p4j>j{AR8dsc07t*1ZJZ z_M`3Pvv;;F8r*o=*5{T^*}yJo-f|2dUd-G56dkK83%WtAkER<3vFa=a^bgNuqjDZ{ zYWGgBy~I^VM0Lnn*}&RmtYy97qO} zSx^n_FmAdRCP%8Jlu4^?A+Th;QI4l$tu13!R8$Ma$Y=_3A=sLO)$_Ny+pqfLm&cA` zxSUp!)=Ck9v>=5Bz6GLdV+O%KaII ztXK)7iDUV)+l!1_vM*b6L4F?EL5SB3%s28A+J>|*22gt0fg;y}I4UdjSwQKh8&K>6 zs?8o)L=r*(!ihdy45gx|5?M?k&N>N0=;bkJYKKV*U&9ocpf~E zcBOkZiuM+wr}NX)CPdHYDO%nFQc@{aP49kwSp5hnG1-dlW$P>Mj9>pKCst?if4r-} z8`Nk9upBFFzpa04VSLVgIT9UfWt@={O?)U;rsLKbV~QjLlPiG9tyamCPTGJ}w4YC7 zUz21kt*~jDWaRMSJ8`1Dd^XRraML87-n^ehmi#aRm5&rBPXm<(q6T&07cHplat z^~@%*)#zaA>$m{oE4_);k9~3S^mt_uJ@W3@5NK-R42GSLW|<^|4kP$O3;z~ijG(3+ zAbJ4|E^G^o?!Rh(o%6dRemxQQrx&BJgdBlZj1bGBUc~At?4DT8619w+9-=3p7!^Pe zM?jF_?+A9yk9ew?+=IrT)DWb`UElm>B$t;2vRRuQOhVUhz6YcagJ z+1_lDL|xq6&_HRnLe9gp+I?X}Gl*0GOfTJx9BoK%!iK;V1(WpR+^F!((7*}2TH|-4 zlqhL6R$@Qo(1kk7Q=$kgz%o5p(JBlmUjfQIcqw7)ZCL2VrBmD5(Ic&iv?I_4A(-I6 z?szvH(DN`JI8_DAiv6|dx1B+4TZcwU_R_kp$e(=*V=@uOWL)sury1g}vy6I8oH5%S zmLnp5PP^|NIK49qre%Wy2VegJ=3N_?Q4G0D;+$n6P{CgU>j9j zJ+Mhr!22IBVdvbvvvJ>&P~mT)VEk@ebkHKu??|Uso8C0X{{+jV0EXNq1G$!w)De!) zvsqET0P^zyY@gGpf2q}3gN=UyD1E!G;EByxDCbe5tDqggc32T2=som3zT~IccTt9P zI~<1k^#O8tHXxEsVYY_m@J=l~IE`ixEpAc?KtUGtfQAT~B&mK%7z!EfIit{*keX<3+d9 zJo}8z7m-I0j{(Gc0nkla8~1C$kGB=HyfRnJi$%vIwhG~$@!y7W_Pij^01T-Xf=@g? zs)3*{a|hHjMlKA@xd$@9!2RfgJHXnfeNQD2K3``7AZ3(u%Id8SG&f>=fI8PmiZ#^>C+TH{?ZHCKUr^m zl`?X|0MwuSL}ZcJ0#LDimhf<%R>kENvx)yk(G?xzFoYndS zWh`PlGktYzCx+M*h5r1>e3%m}%@Z+NuzAHXvt_04D9d zE9bF&zlV12z@uAREG>IU>b}-~W4?z@OXvRm2qKFMBI`1tU_u#Pg)zVlDDYvp%F2rE zuL-4lc4og5O5bv66Ri?RgA+U#6!)ki_VNO+IUx3`lVa_A0Cljl9(|4G-iMIj86})z zv|q`q!Cz{-lMvUMP;6O!Fx%edTEP>bpVI4k(cl3g^*KWh{L`KvVQu+`4qux;U)shE ziYH7F?urNH{~>LEowAuh4G@C#6X1zR&Em+Ppjvh6rB$d#U{kq&lLG5)Fl%@!>h%$C zDLm6129fz_oCbq{h&zU7#E(Ey+(R7k0KsjEp!{+$(R5IeO?P*DbkM>0PK>UKfx&uk zbCBdg(2r1QwM-_cpBtFv`c)y!YfpOJFB&Xq);X?r`&BXTYeZD^5@YY@_;p(F>^7*_ zpArc)x+n<6jO7$* zHDmqp6W5bv_(=Mc7Xe;jDx+fowOL6}ge|ak3e;Z^3?Ps=cW|~7n%(t|JJf`BJFW;F z4rtek`;F`F<#TnrP^Jy8AR(z3_f1KUKADJH(KB)za_A_aQ3_0bL|Ts*1e_TpRu4Lm z_Bdxtnuhihlr-;92(Ee<3XxSeN;AYKIfXQ$lKxB}x}=Ne zA=sA)8_)fFiZE#bO}p1%2h{WD<}Tt-10+Db=O2?DWJ;1pDTVN|2gOuP69~oHkr!ll z=1({;LGB}A1Al?=OoAfBTOEp)NQi$=^x4Atb|tdnwsW`}hVPb2z<-1_A)Xn$TnU`% zs+3nlu`CR8;lg^~Y?R)2yF#(F`ZYG&mp%f{CSccl&a8@dl%Nme@r;XAt1_&fieakP z6Cln>Byk&1tz}$!x74M2n+lqYJ7uFD8C`W>n0o|rJV{DyeTs`H7)+n5`Kjh3=N4); z@&sq!)3vpDc}-UF^1LK|bCw)$Uiffr6iSN%esBPUmm#pV5fs=N?>0Y6^SQW#j)n;) zTRQlS_BK$9rvw=>DV#X7wCUR%_ZZv%Rq_PP0qMH3oI<6**G; zcEaA-Gnbcxy5gJI2FRfV62fm^Fq*qsm>%6BYs6U)OFA=cD!<>X@CZZM05Ea++$uU; z>TF+vKjn5GvFwq#87qK=?Kl9r%)oV@^vx?1Fm5BTdOB7%XH0Zu!~$HUjd3i|i*03q z!f&4h_q72ySW8Z99n5FxF=T*60VsMb-2kwbQKbhzaQ4wpQQ>D^4b1bCWhl6d-8DLf ze@;-h{yVDL^Puicxj}!Lr|;c^2PY)7`_fURvh;^sy3>@8Vl48zHztIlqjyD|$({Wm z4y>8)#pPVpqu)XZkUBnbJiW?{ckiXOYv@1{<6q$Cd!YO|Qvh|{=$w1gRd@Q;0UY(< zR8s)OK)sTO?M9uc<3^pQc9u>YeO)nc02TA;alxr^@3>9@Q!nmOv`DbZjkJ!-6=pr9~>7>Ii$3;T zfb7Zu<|NL?oUb;G8&L9|@#6#fTaVg)2LSy?IG&*CpvC;94oN@ZukS)irS=xP4RCv~ zc+_JNuY1{vbQ6_v0wV-FxlAG71?bVc``k`W`Cg+d>o$D8?E5|MxPZ$%5y}C9V7KSO zLv%V+Py!zW1!nCk-)+V-KON+fJH|ZfK1GnkzaT?;GiH|l zp@E{i*bjvRkGx)O$6o!rxK;o`4`OWZdfqYHx$bB*;}Bw7J|ia(gQ*Z@mgCgafMSiJ2%^JeP!BJa42WNzCZCZE!v z7ETF&AK!tcaDe+5cJ*1k0s-57+``Q*Ep>=gR6HZSOfzzPRaEj%lWJ=hbsEPcqDceN zxPPApU&D6S`@l*BxLTgHnrRI8@6nk(Z!C#=-w;*QaKmvg2e$~j->`C4xr~>=-hKhq zDKMRPBU6f+i6|4QLRh5g|GgABH}8jk)l19q4P_#+T?dPd#|u~v@6)av$NvH--6HxW zt;TR;Ty5BH*52Oi7XD92X%plA>M7jl4Bc-8yX}eUmp)BlZ!Y?xxUW$a+#Z@hEF70+ zle}vN^DXZw0)Dd^9dsx4BIre{&=E1BZSb*Z1>< zp7N?iYaeD2t>EoicqKX6I?IkpqS7Z(*A4)@nC3R*9=;t(vm2U7okO=LX}D}7$Y|6o zzBzf`#y#%E6uxWU8ciS1<~Rw}$$U^U=uTWjyRVMe;>~dcg1ALZ@rv$PqRzW?ddwF^ z@Rhbjiv1(ktiCzk+?P%(1KR*oqRv(T@riq94-In3R+n+h*&HbV?R~Y2%$OH#Lrs%Q zwGsJ=Q2a8(v|R`HypdJW9_G}>xYNBDO_;RUj%`MwVZE8ufeG`{2O<;=(*RjWdJ<{7 z80s4bX~?nVkJoR@0Nc~Q#1c}9sPZ##>`17OL~w$=7!EBk%Aa1^!cu?~vNX(iFDLNH zSs)6QyM?ENMSy?jmpUcyFtwK@vo3)1sa+puCNnyP5xmQQ=U57{afikiR-$I3ask?6nriFYR8D&F6U`H=6^LF9HA!3XXIEU$m z-PxxQXD?xvPY?xuaKtBqkT7ev9eW)CLi)V}6poI%R^lG4t)fCX{LGHEC5+o95r*f- zCT$_Xv(MD17X(!Bm3)-Jsex2AFf&246RBq#K3X6>2|rW~B4xv)@-Gvt2otVls7V1fp0$$&XgE6n@#&WWI;r^Z0@v^Z_8Se;Up8w_x8*DdtS%rwpbB3tz3 z2-z^Fq~eCKSImc(x++>CEnxGUG^8I)5USSOW)UJiP$e?XZM=w(38Dr*8Y?QqS(ih{ zqEX>Fk6|U2C+G-igc;8osXgxH0gHYTJGZ$(+sakc6};fXy$}Ypn3ubtMSI+hpiHHj z{)dE~kVcxa>JI$;-6B4WBbg`X+I+eh=gtMPpd-!EozZ3yQBqp|=z<&wyE<()x zFFZm*JGMZzf5R;zcZm$Q1WA1iN|~tl*pA5?r5t=P8ztDk^OL!O9zFN1GK#6opoU&F51nykp^mlO!og)My zsET--37;~XVQVNWDcAR*52n_n`xdZ3|9DPs-8DEph%e}KFcs0-n@v3gESgo;(QcEj zywWS(tN(J5j~o`g;<$e^1yMs~yJM4)lMGYJdv4(b+DBdSZq#_Q!n_8VR1)V998&Rm zxL#>JrFh=tQW}{9v4g6JU)Mfs!{u4W!nH0>D2gNxgneE&k}uG>ElNFjEv2rNy?a!q z<(N8E#m})cIXN2gTIvSiHaZQJ1{E(ya57b6^>M2DQp4b71OpMn=dEV?&r&LJoN9eT zu;vZBq@2It?PYU)v4)ASC^0f-%rJD}bKeI`8`CKAo{*_p=_?bMyF4RGF9hY{nf b29HP@D9EFJDBd}ZfIn(VT8gjb%^v(OKI6J9 literal 0 HcmV?d00001 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 new file mode 100644 index 00000000..57a8452b --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..c6aafd11 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { + CheckIcon, + CopyIcon, + ExternalLinkIcon, + GithubIcon, + TerminalIcon, +} from 'lucide-react'; +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; + +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+Geist + {' '} + 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 new file mode 100644 index 00000000..dc98be74 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/app/globals.css @@ -0,0 +1,122 @@ +@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 new file mode 100644 index 00000000..1061d26d --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/app/layout.tsx @@ -0,0 +1,35 @@ +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 new file mode 100644 index 00000000..887073db --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/app/navigation.tsx @@ -0,0 +1,12 @@ +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 new file mode 100644 index 00000000..c1cd2554 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppLogo.tsx @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..d842e03c --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx @@ -0,0 +1,23 @@ +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 new file mode 100644 index 00000000..2733308e --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.module.css @@ -0,0 +1,33 @@ +.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 new file mode 100644 index 00000000..a302ecce --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx @@ -0,0 +1,30 @@ +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 new file mode 100644 index 00000000..ac2a2e2b --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx @@ -0,0 +1,25 @@ +"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 new file mode 100644 index 00000000..06ce2676 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx @@ -0,0 +1,35 @@ +"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 new file mode 100644 index 00000000..ded639d0 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/internal/config.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..2de3b630 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-center.tsx @@ -0,0 +1,13 @@ +/** + * 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 new file mode 100644 index 00000000..781fcbce --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-left.tsx @@ -0,0 +1,23 @@ +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 new file mode 100644 index 00000000..f63d0365 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-mobile-content.tsx @@ -0,0 +1,43 @@ +"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 new file mode 100644 index 00000000..afe06352 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/AppShell/slot-header-right.tsx @@ -0,0 +1,25 @@ +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 new file mode 100644 index 00000000..bff50676 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/mode-toggle.tsx @@ -0,0 +1,39 @@ +"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 new file mode 100644 index 00000000..a101d447 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/providers.tsx @@ -0,0 +1,13 @@ +"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 new file mode 100644 index 00000000..6459132f --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/theme-provider.tsx @@ -0,0 +1,11 @@ +"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 new file mode 100644 index 00000000..30f83b65 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/ui/button.tsx @@ -0,0 +1,61 @@ +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 new file mode 100644 index 00000000..d1c32758 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,267 @@ +"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 new file mode 100644 index 00000000..79926117 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/components/ui/sonner.tsx @@ -0,0 +1,31 @@ +"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 new file mode 100644 index 00000000..83518a22 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/lib/env.ts @@ -0,0 +1,12 @@ +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 new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/src/lib/utils.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..dd41d9d9 --- /dev/null +++ b/packages/cli-old/template/nextjs-shadcn/tsconfig.json @@ -0,0 +1,41 @@ +{ + "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 new file mode 100644 index 00000000..dcdbd2be --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/blank/page.tsx @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000..20dcfa92 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-edit/actions.ts @@ -0,0 +1,24 @@ +"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 new file mode 100644 index 00000000..5957658b --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-edit/page.tsx @@ -0,0 +1,28 @@ +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 new file mode 100644 index 00000000..28c55f5c --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-edit/schema.ts @@ -0,0 +1,4 @@ +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 new file mode 100644 index 00000000..2166994f --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-edit/table.tsx @@ -0,0 +1,45 @@ +"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 new file mode 100644 index 00000000..db312817 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite-edit/actions.ts @@ -0,0 +1,84 @@ +"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 new file mode 100644 index 00000000..d194b139 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite-edit/page.tsx @@ -0,0 +1,23 @@ +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 new file mode 100644 index 00000000..faf1675a --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite-edit/query.ts @@ -0,0 +1,87 @@ +"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 new file mode 100644 index 00000000..28c55f5c --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite-edit/schema.ts @@ -0,0 +1,4 @@ +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 new file mode 100644 index 00000000..aeb29899 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite-edit/table.tsx @@ -0,0 +1,130 @@ +"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 new file mode 100644 index 00000000..68f30627 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite/actions.ts @@ -0,0 +1,62 @@ +"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 new file mode 100644 index 00000000..990ef802 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite/page.tsx @@ -0,0 +1,11 @@ +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 new file mode 100644 index 00000000..d4c0e26a --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite/query.ts @@ -0,0 +1,45 @@ +"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 new file mode 100644 index 00000000..d76daa43 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table-infinite/table.tsx @@ -0,0 +1,108 @@ +"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 new file mode 100644 index 00000000..da3dae96 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table/page.tsx @@ -0,0 +1,17 @@ +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 new file mode 100644 index 00000000..52479327 --- /dev/null +++ b/packages/cli-old/template/pages/nextjs/table/table.tsx @@ -0,0 +1,18 @@ +"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 new file mode 100644 index 00000000..e69de29b 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 new file mode 100644 index 00000000..0db5cb10 --- /dev/null +++ b/packages/cli-old/template/pages/vite-wv/table-edit/index.tsx @@ -0,0 +1,72 @@ +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 new file mode 100644 index 00000000..6f09d89c --- /dev/null +++ b/packages/cli-old/template/pages/vite-wv/table/index.tsx @@ -0,0 +1,35 @@ +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 new file mode 100644 index 00000000..469bea0f --- /dev/null +++ b/packages/cli-old/template/vite-wv/.claude/launch.json @@ -0,0 +1,18 @@ +{ + "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 new file mode 100644 index 00000000..00b5278e --- /dev/null +++ b/packages/cli-old/template/vite-wv/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "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 new file mode 100644 index 00000000..6b2924ba --- /dev/null +++ b/packages/cli-old/template/vite-wv/AGENTS.md @@ -0,0 +1 @@ +__AGENT_INSTRUCTIONS__ diff --git a/packages/cli-old/template/vite-wv/CLAUDE.md b/packages/cli-old/template/vite-wv/CLAUDE.md new file mode 100644 index 00000000..c3170642 --- /dev/null +++ b/packages/cli-old/template/vite-wv/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md diff --git a/packages/cli-old/template/vite-wv/_gitignore b/packages/cli-old/template/vite-wv/_gitignore new file mode 100644 index 00000000..984db15a --- /dev/null +++ b/packages/cli-old/template/vite-wv/_gitignore @@ -0,0 +1,19 @@ +# 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 new file mode 100644 index 00000000..13e1db0b --- /dev/null +++ b/packages/cli-old/template/vite-wv/components.json @@ -0,0 +1,21 @@ +{ + "$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 new file mode 100644 index 00000000..93e935bb --- /dev/null +++ b/packages/cli-old/template/vite-wv/index.html @@ -0,0 +1,13 @@ + + + + + + ProofKit WebViewer Starter + + + +
+ + + diff --git a/packages/cli-old/template/vite-wv/package.json b/packages/cli-old/template/vite-wv/package.json new file mode 100644 index 00000000..13a1ee7e --- /dev/null +++ b/packages/cli-old/template/vite-wv/package.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 00000000..bed0eafc --- /dev/null +++ b/packages/cli-old/template/vite-wv/proofkit-typegen.config.jsonc @@ -0,0 +1,18 @@ +{ + "$schema": "https://proofkit.dev/typegen-config-schema.json", + "config": { + "type": "fmdapi", + "path": "./src/config/schemas/filemaker", + "clearOldFiles": true, + "clientSuffix": "Layout", + "validator": "zod/v4", + "webviewerScriptName": "ExecuteDataApi", + "fmHttp": { + "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 new file mode 100644 index 00000000..3bd029c2 --- /dev/null +++ b/packages/cli-old/template/vite-wv/proofkit.json @@ -0,0 +1,9 @@ +{ + "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 new file mode 100644 index 00000000..172f9c3d --- /dev/null +++ b/packages/cli-old/template/vite-wv/scripts/filemaker.js @@ -0,0 +1,96 @@ +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 defaultFmHttpBaseUrl = process.env.FM_HTTP_BASE_URL ?? "http://127.0.0.1:1365"; + +function stripFileExtension(fileName) { + return fileName.replace(/\.fmp12$/i, ""); +} + +async function getConnectedFiles(baseUrl = defaultFmHttpBaseUrl) { + 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-http", + }; + } + + 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 HTTP.`, + ); + } + } + + if (connectedFiles.length === 1) { + return { + fileName: stripFileExtension(connectedFiles[0]), + host: "$", + source: "fm-http", + }; + } + + if (connectedFiles.length > 1) { + throw new Error( + `Multiple FileMaker files are connected via FM HTTP (${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 new file mode 100644 index 00000000..638e2f1a --- /dev/null +++ b/packages/cli-old/template/vite-wv/scripts/launch-fm.js @@ -0,0 +1,19 @@ +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 HTTP 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 new file mode 100644 index 00000000..cce9457d --- /dev/null +++ b/packages/cli-old/template/vite-wv/scripts/upload.js @@ -0,0 +1,24 @@ +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 HTTP 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 new file mode 100644 index 00000000..b7b39848 --- /dev/null +++ b/packages/cli-old/template/vite-wv/src/App.tsx @@ -0,0 +1,84 @@ +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 HTTP 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 WebViewer Starter +
+ +
+
+

+ React + TypeScript + Vite +

+

+ Build browser-safe FileMaker WebViewer 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 new file mode 100644 index 00000000..6a1d0b1f --- /dev/null +++ b/packages/cli-old/template/vite-wv/src/index.css @@ -0,0 +1,96 @@ +@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 new file mode 100644 index 00000000..a5ef1935 --- /dev/null +++ b/packages/cli-old/template/vite-wv/src/lib/utils.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..f61aedf6 --- /dev/null +++ b/packages/cli-old/template/vite-wv/src/main.tsx @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..d21c9dbe --- /dev/null +++ b/packages/cli-old/template/vite-wv/src/router.tsx @@ -0,0 +1,57 @@ +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 new file mode 100644 index 00000000..833e629c --- /dev/null +++ b/packages/cli-old/template/vite-wv/src/routes/query-demo.tsx @@ -0,0 +1,37 @@ +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 WebViewer + 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 new file mode 100644 index 00000000..1565cf30 --- /dev/null +++ b/packages/cli-old/template/vite-wv/tsconfig.json @@ -0,0 +1,16 @@ +{ + "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 new file mode 100644 index 00000000..8e23f082 --- /dev/null +++ b/packages/cli-old/template/vite-wv/vite.config.ts @@ -0,0 +1,18 @@ +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/tests/browser-apps.test.ts b/packages/cli-old/tests/browser-apps.smoke.test.ts similarity index 92% rename from packages/cli/tests/browser-apps.test.ts rename to packages/cli-old/tests/browser-apps.smoke.test.ts index 2553ec76..ef7d1c3a 100644 --- a/packages/cli/tests/browser-apps.test.ts +++ b/packages/cli-old/tests/browser-apps.smoke.test.ts @@ -4,16 +4,16 @@ import { join } from "node:path"; import { beforeEach, describe, expect, it } from "vitest"; import { z } from "zod/v4"; -import { verifyProjectBuilds } from "./test-utils"; +import { verifySmokeProjectBuilds } from "./test-utils"; -describe("Non-Interactive CLI Tests", () => { +describe("External integration smoke tests (non-interactive CLI)", () => { // 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); - // Parse test environment variables + // Required for live Otto/FileMaker integration smoke coverage. const testEnv = z .object({ OTTO_SERVER_URL: z.url(), @@ -82,6 +82,6 @@ describe("Non-Interactive CLI Tests", () => { ); // Verify the project can be built successfully - verifyProjectBuilds(projectDir); + verifySmokeProjectBuilds(projectDir); }); }); diff --git a/packages/cli-old/tests/cli.test.ts b/packages/cli-old/tests/cli.test.ts new file mode 100644 index 00000000..f86d3fb4 --- /dev/null +++ b/packages/cli-old/tests/cli.test.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 00000000..af855a83 --- /dev/null +++ b/packages/cli-old/tests/init-non-interactive-failures.test.ts @@ -0,0 +1,222 @@ +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 new file mode 100644 index 00000000..f13e9dff --- /dev/null +++ b/packages/cli-old/tests/init-post-init-generation-errors.test.ts @@ -0,0 +1,62 @@ +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 new file mode 100644 index 00000000..6422d97b --- /dev/null +++ b/packages/cli-old/tests/init-run-init-regression.test.ts @@ -0,0 +1,197 @@ +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 new file mode 100644 index 00000000..3edefc51 --- /dev/null +++ b/packages/cli-old/tests/init-scaffold-contract.test.ts @@ -0,0 +1,220 @@ +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; + fmHttp?: { + 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?.fmHttp?.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 new file mode 100644 index 00000000..9f7ba3cd --- /dev/null +++ b/packages/cli-old/tests/setup.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..7c8ce0b9 --- /dev/null +++ b/packages/cli-old/tests/test-utils.ts @@ -0,0 +1,70 @@ +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 new file mode 100644 index 00000000..831766c2 --- /dev/null +++ b/packages/cli-old/tests/webviewer-apps.test.ts @@ -0,0 +1,155 @@ +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("WebViewer 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 new file mode 100644 index 00000000..5e73ded4 --- /dev/null +++ b/packages/cli-old/tsconfig.json @@ -0,0 +1,14 @@ +{ + "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 new file mode 100644 index 00000000..c9df787c --- /dev/null +++ b/packages/cli-old/tsdown.config.ts @@ -0,0 +1,54 @@ +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 ? "http://localhost:3005" : "https://proofkit.dev"), + }, + }), + ], + onSuccess: isDev ? "node dist/index.js" : undefined, +}); diff --git a/packages/cli-old/vitest.config.ts b/packages/cli-old/vitest.config.ts new file mode 100644 index 00000000..c64f5913 --- /dev/null +++ b/packages/cli-old/vitest.config.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000..dae55ef3 --- /dev/null +++ b/packages/cli-old/vitest.smoke.config.ts @@ -0,0 +1,18 @@ +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, + }, +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index 4a3c78bd..2f2a8cf5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@proofkit/cli", - "version": "2.0.0-beta.22", - "description": "Create web application with the ProofKit stack", + "version": "2.0.0-beta.0", + "description": "Interactive CLI to scaffold and manage ProofKit projects", "license": "MIT", "repository": { "type": "git", @@ -30,9 +30,8 @@ "dist", "template", "README.md", - "index.d.ts", - "LICENSE", "CHANGELOG.md", + "index.d.ts", "package.json" ], "engines": { @@ -51,12 +50,18 @@ "pub:beta": "NODE_ENV=production pnpm build && npm publish --tag beta --access public", "pub:next": "NODE_ENV=production pnpm build && npm publish --tag next --access public", "pub:release": "NODE_ENV=production pnpm build && npm publish --access public", - "test": "vitest run" + "test": "vitest run", + "test:smoke": "PROOFKIT_RUN_SMOKE_TESTS=1 vitest run --config vitest.smoke.config.ts" }, "dependencies": { "@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", + "@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:*", @@ -65,6 +70,7 @@ "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", @@ -80,7 +86,8 @@ "semver": "^7.7.3", "shadcn": "^2.10.0", "sort-package-json": "^2.15.1", - "ts-morph": "^26.0.0" + "ts-morph": "^26.0.0", + "type-fest": "^3.13.1" }, "devDependencies": { "@auth/drizzle-adapter": "^1.11.1", @@ -119,10 +126,12 @@ "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": "public" } } diff --git a/packages/cli/src/cli/init.ts b/packages/cli/src/cli/init.ts index 0196194b..4b8cc21c 100644 --- a/packages/cli/src/cli/init.ts +++ b/packages/cli/src/cli/init.ts @@ -134,6 +134,59 @@ type ProofKitPackageJSON = PackageJson & { }; }; +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; @@ -157,6 +210,21 @@ export const runInit = async (name?: string, opts?: CliFlags) => { ).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" @@ -179,6 +247,21 @@ export const runInit = async (name?: string, opts?: CliFlags) => { ) 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"] @@ -286,19 +369,18 @@ export const runInit = async (name?: string, opts?: CliFlags) => { } if (dataSource === "filemaker") { - const hasExplicitFileMakerInputs = Boolean( - cliOptions.server || - cliOptions.adminApiKey || - cliOptions.dataApiKey || - cliOptions.fileName || - cliOptions.layoutName || - cliOptions.schemaName, - ); - - const shouldSkipInitialCodegen = state.appType === "webviewer" && nonInteractive && !hasExplicitFileMakerInputs; + const shouldRunInitialCodegen = state.appType === "webviewer" && !(nonInteractive && !hasExplicitFileMakerInputs); - if (!shouldSkipInitialCodegen) { - await runCodegenCommand(); + if (shouldRunInitialCodegen) { + try { + await runCodegenCommand(); + } catch (error) { + throw createPostInitGenerationError({ + error, + appType: state.appType ?? "browser", + projectDir, + }); + } } } diff --git a/packages/cli/src/consts.ts b/packages/cli/src/consts.ts index 8c9ba3ed..6f544052 100644 --- a/packages/cli/src/consts.ts +++ b/packages/cli/src/consts.ts @@ -1,39 +1,49 @@ +import fs from "node:fs"; 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 DEFAULT_APP_NAME = "my-proofkit-app"; export const cliName = "proofkit"; export const npmName = "@proofkit/cli"; export const DOCS_URL = "https://proofkit.dev"; -const version = getVersion(); -const versionCharLength = version.length; -//export const PKG_ROOT = path.dirname(require.main.filename); +export const AGENT_INSTRUCTIONS = [ + "Use the ProofKit docs as the primary reference for this project: https://proofkit.dev/docs", + "Before doing any AI-assisted development here, run `npx @tanstack/intent@latest install` in the project root to load skills relevant to this project", +].join("\n"); -export const TITLE_TEXT = ` +declare const __REGISTRY_URL__: string; +export const DEFAULT_REGISTRY_URL = + typeof __REGISTRY_URL__ !== "undefined" && __REGISTRY_URL__ ? __REGISTRY_URL__ : "https://proofkit.dev"; +const TITLE_ASCII = ` _______ ___ ___ ____ _ _ |_ __ \\ .' ..]|_ ||_ _| (_) / |_ | |__) |_ .--. .--. .--. _| |_ | |_/ / __ \`| |-' | ___/[ \`/'\`\\]/ .'\`\\ \\/ .'\`\\ \\'-| |-' | __'. [ | | | _| |_ | | | \\__. || \\__. | | | _| | \\ \\_ | | | |, |_____| [___] '.__.' '.__.' [___] |____||____|[___]\\__/ -${" ".repeat(61 - versionCharLength)}v${version} `; -export const DEFAULT_APP_NAME = "my-proofkit-app"; -export const CREATE_FM_APP = cliName; -export const AGENT_INSTRUCTIONS = [ - "Use the ProofKit docs as the primary reference for this project: https://proofkit.dev/docs", - "Before doing any AI-assisted development here, run `npx @tanstack/intent@latest install` in the project root to load skills relevant to this project", -].join("\n"); +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`; +} -// Registry URL is injected at build time via tsdown define -declare const __REGISTRY_URL__: string; -// Provide a safe fallback when running from source (not built) -export const DEFAULT_REGISTRY_URL = - // typeof check avoids ReferenceError if not defined at runtime - typeof __REGISTRY_URL__ !== "undefined" && __REGISTRY_URL__ ? __REGISTRY_URL__ : "https://proofkit.dev"; +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/new/src/core/context.ts b/packages/cli/src/core/context.ts similarity index 94% rename from packages/new/src/core/context.ts rename to packages/cli/src/core/context.ts index abfa8887..6da8bdcc 100644 --- a/packages/new/src/core/context.ts +++ b/packages/cli/src/core/context.ts @@ -14,7 +14,7 @@ export interface CliContextValue { }; } -export const CliContext = Context.GenericTag("@proofkit/new/CliContext"); +export const CliContext = Context.GenericTag("@proofkit/cli/CliContext"); export interface PromptService { readonly text: (options: { @@ -43,7 +43,7 @@ export interface PromptService { readonly confirm: (options: { message: string; initialValue?: boolean }) => Promise; } -export const PromptService = Context.GenericTag("@proofkit/new/PromptService"); +export const PromptService = Context.GenericTag("@proofkit/cli/PromptService"); export interface ConsoleService { readonly info: (message: string) => void; @@ -53,7 +53,7 @@ export interface ConsoleService { readonly note: (message: string, title?: string) => void; } -export const ConsoleService = Context.GenericTag("@proofkit/new/ConsoleService"); +export const ConsoleService = Context.GenericTag("@proofkit/cli/ConsoleService"); export interface FileSystemService { readonly exists: (path: string) => Promise; @@ -68,19 +68,19 @@ export interface FileSystemService { readonly readFile: (path: string) => Promise; } -export const FileSystemService = Context.GenericTag("@proofkit/new/FileSystemService"); +export const FileSystemService = Context.GenericTag("@proofkit/cli/FileSystemService"); export interface TemplateService { readonly getTemplateDir: (appType: AppType, ui: UIType) => string; } -export const TemplateService = Context.GenericTag("@proofkit/new/TemplateService"); +export const TemplateService = Context.GenericTag("@proofkit/cli/TemplateService"); export interface PackageManagerService { readonly getVersion: (packageManager: PackageManager, cwd: string) => Promise; } -export const PackageManagerService = Context.GenericTag("@proofkit/new/PackageManagerService"); +export const PackageManagerService = Context.GenericTag("@proofkit/cli/PackageManagerService"); export interface ProcessService { readonly run: ( @@ -94,13 +94,13 @@ export interface ProcessService { ) => Promise<{ stdout: string; stderr: string }>; } -export const ProcessService = Context.GenericTag("@proofkit/new/ProcessService"); +export const ProcessService = Context.GenericTag("@proofkit/cli/ProcessService"); export interface GitService { readonly initialize: (projectDir: string) => Promise; } -export const GitService = Context.GenericTag("@proofkit/new/GitService"); +export const GitService = Context.GenericTag("@proofkit/cli/GitService"); export interface SettingsService { readonly writeSettings: (projectDir: string, settings: ProofKitSettings) => Promise; @@ -111,7 +111,7 @@ export interface SettingsService { ) => Promise; } -export const SettingsService = Context.GenericTag("@proofkit/new/SettingsService"); +export const SettingsService = Context.GenericTag("@proofkit/cli/SettingsService"); export interface FmHttpStatus { baseUrl: string; @@ -199,10 +199,10 @@ export interface FileMakerService { ) => Promise; } -export const FileMakerService = Context.GenericTag("@proofkit/new/FileMakerService"); +export const FileMakerService = Context.GenericTag("@proofkit/cli/FileMakerService"); export interface CodegenService { readonly runInitial: (projectDir: string, packageManager: PackageManager) => Promise; } -export const CodegenService = Context.GenericTag("@proofkit/new/CodegenService"); +export const CodegenService = Context.GenericTag("@proofkit/cli/CodegenService"); diff --git a/packages/new/src/core/errors.ts b/packages/cli/src/core/errors.ts similarity index 100% rename from packages/new/src/core/errors.ts rename to packages/cli/src/core/errors.ts diff --git a/packages/new/src/core/executeInitPlan.ts b/packages/cli/src/core/executeInitPlan.ts similarity index 78% rename from packages/new/src/core/executeInitPlan.ts rename to packages/cli/src/core/executeInitPlan.ts index 104df9cd..ed5470af 100644 --- a/packages/new/src/core/executeInitPlan.ts +++ b/packages/cli/src/core/executeInitPlan.ts @@ -1,7 +1,9 @@ import path from "node:path"; +import { Chalk } from "chalk"; import { Effect } from "effect"; import sortPackageJson from "sort-package-json"; +import { AGENT_INSTRUCTIONS } from "~/consts.js"; import { CliContext, CodegenService, @@ -22,6 +24,48 @@ import { normalizeImportAlias, replaceTextInFiles } from "~/utils/projectFiles.j 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); + +function renderNextSteps(plan: InitPlan) { + const lines = [ + `${formatHeading("Project root:")} ${formatCommand(`cd ${formatPath(plan.request.appDir)}`)}`, + "", + formatHeading("Agent setup:"), + "Have your agent run this in the new project and complete the interactive prompt so it can load the right skills:", + ` ${formatCommand("npx @tanstack/intent@latest install")}`, + ]; + + if (plan.request.noInstall) { + lines.push( + "", + formatHeading("Install dependencies:"), + ` ${formatCommand(plan.request.packageManager === "yarn" ? "yarn" : `${plan.request.packageManager} install`)}`, + ); + } + + 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`)}`, + ); + } + + lines.push( + "", + formatHeading("More ProofKit commands:"), + ` ${formatCommand(`${plan.packageManagerCommand} proofkit`)}`, + ); + + return lines.join("\n"); +} function getMeaningfulDirectoryEntries(entries: string[]) { return entries.filter((entry) => { @@ -139,6 +183,10 @@ export const executeInitPlan = (plan: InitPlan) => } yield* Effect.promise(() => replaceTextInFiles(fs, plan.targetDir, "__PNPM_COMMAND__", plan.packageManagerCommand)); + yield* Effect.promise(() => + replaceTextInFiles(fs, plan.targetDir, "__PACKAGE_MANAGER__", plan.request.packageManager), + ); + yield* Effect.promise(() => replaceTextInFiles(fs, plan.targetDir, "__AGENT_INSTRUCTIONS__", AGENT_INSTRUCTIONS)); if (plan.request.importAlias !== "~/") { yield* Effect.promise(() => replaceTextInFiles(fs, plan.targetDir, "~/", normalizeImportAlias(plan.request.importAlias)), @@ -195,6 +243,7 @@ export const executeInitPlan = (plan: InitPlan) => packageManagerVersion ? ` using ${plan.request.packageManager}@${packageManagerVersion}` : "" }`, ); - consoleService.note(plan.nextSteps.map((step) => ` ${step}`).join("\n"), "Next steps"); + consoleService.info(chalk.bold("Next steps:")); + consoleService.info(renderNextSteps(plan)); return plan; }); diff --git a/packages/new/src/core/planInit.ts b/packages/cli/src/core/planInit.ts similarity index 94% rename from packages/new/src/core/planInit.ts rename to packages/cli/src/core/planInit.ts index fb33c7cc..207ba535 100644 --- a/packages/new/src/core/planInit.ts +++ b/packages/cli/src/core/planInit.ts @@ -46,10 +46,11 @@ export function planInit( : undefined, proofkitMetadata: { initVersion: getScaffoldVersion(), - scaffoldPackage: "@proofkit/new", + scaffoldPackage: "@proofkit/cli", }, dependencies: {}, devDependencies: { + "@proofkit/cli": releaseTag, "@types/node": `^${getNodeMajorVersion()}`, }, }; @@ -64,9 +65,12 @@ export function planInit( Object.assign(packageJson.dependencies, sharedUiDependencies); packageJson.dependencies["@proofkit/fmdapi"] = releaseTag; packageJson.dependencies["@proofkit/webviewer"] = releaseTag; + packageJson.dependencies["@tanstack/react-query"] = "^5.90.21"; + packageJson.dependencies["@tanstack/react-router"] = "^1.167.4"; packageJson.dependencies.zod = "^4"; packageJson.devDependencies["@proofkit/typegen"] = releaseTag; packageJson.devDependencies["@tailwindcss/vite"] = "^4.2.1"; + packageJson.devDependencies.ultracite = "7.0.8"; } return { @@ -102,6 +106,7 @@ export function planInit( nextSteps: [ `cd ${request.appDir}`, ...(request.noInstall ? [request.packageManager === "yarn" ? "yarn" : `${request.packageManager} install`] : []), + "npx @tanstack/intent@latest install", formatPackageManagerCommand(request.packageManager, "dev"), ...(request.appType === "webviewer" ? [ diff --git a/packages/new/src/core/resolveInitRequest.ts b/packages/cli/src/core/resolveInitRequest.ts similarity index 95% rename from packages/new/src/core/resolveInitRequest.ts rename to packages/cli/src/core/resolveInitRequest.ts index 9d7f17c8..8154452e 100644 --- a/packages/new/src/core/resolveInitRequest.ts +++ b/packages/cli/src/core/resolveInitRequest.ts @@ -423,6 +423,18 @@ export const resolveInitRequest = (name?: string, rawFlags?: CliFlags) => ).pipe(Effect.map((value) => value as DataSourceType)); } + const hasExplicitFileMakerInputs = Boolean( + flags.server || flags.adminApiKey || flags.dataApiKey || flags.fileName || flags.layoutName || flags.schemaName, + ); + + if (nonInteractive && !flags.dataSource && hasExplicitFileMakerInputs) { + return yield* Effect.fail(new Error("FileMaker flags require --data-source filemaker in non-interactive mode.")); + } + + if (nonInteractive && dataSource !== "filemaker" && hasExplicitFileMakerInputs) { + return yield* Effect.fail(new Error("FileMaker flags require --data-source filemaker in non-interactive mode.")); + } + const { fileMaker, skipFileMakerSetup } = yield* Effect.promise(() => resolveFileMakerInputs({ prompt, diff --git a/packages/new/src/core/types.ts b/packages/cli/src/core/types.ts similarity index 100% rename from packages/new/src/core/types.ts rename to packages/cli/src/core/types.ts diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 91a32558..39277255 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,85 +1,401 @@ -#!/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"; +#!/usr/bin/env node +import { readFileSync } 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, + TemplateService, +} from "~/core/context.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 { makeLiveLayer } from "~/services/live.js"; +import { intro } from "~/utils/prompts.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(); - -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); +const defaultCliFlags: CliFlags = { + noGit: false, + noInstall: false, + force: false, + default: false, + CI: false, + importAlias: "~/", }; -main().catch((err) => { - if (err instanceof UserAbortedError) { - process.exit(0); - } else if (err instanceof Error) { - logger.error("Aborting installation..."); - logger.error(err); - } else { - logger.error("An unknown error has occurred. Please open an issue on github with the below:"); - console.log(err); +function getCliVersion() { + try { + const packageJsonUrl = new URL("../package.json", import.meta.url); + const packageJson = JSON.parse(readFileSync(fileURLToPath(packageJsonUrl), "utf8")) as { version?: string }; + return packageJson.version ?? "0.0.0-private"; + } catch { + return "0.0.0-private"; } - process.exit(1); +} + +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 packageManagerVersion = yield* Effect.promise(() => + packageManagerService.getVersion(request.packageManager, request.cwd), + ); + const plan = planInit(request, { templateDir, packageManagerVersion }); + yield* executeInitPlan(plan); + return { request, plan }; + }); + +export const runDefaultCommand = (rawFlags?: Partial) => + Effect.gen(function* () { + const cliContext = yield* CliContext; + const fsService = yield* FileSystemService; + const consoleService = yield* ConsoleService; + const flags = { ...defaultCliFlags, ...rawFlags }; + + if (cliContext.nonInteractive || flags.CI || flags.nonInteractive) { + throw new Error( + "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit init --non-interactive`.", + ); + } + + const settingsPath = path.join(cliContext.cwd, "proofkit.json"); + const hasProofKitProject = yield* Effect.promise(() => fsService.exists(settingsPath)); + + if (hasProofKitProject) { + intro(`Found ${proofGradient("ProofKit")} project`); + consoleService.note( + [ + "Project command routing is being migrated into the Effect CLI surface.", + "Use an explicit command such as `proofkit add`, `proofkit remove`, `proofkit typegen`, `proofkit deploy`, or `proofkit upgrade`.", + ].join("\n"), + "Project commands", + ); + return; + } + + 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"), +); + +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 legacyEffect(runLegacy: () => Promise, options?: { nonInteractive?: boolean; debug?: boolean }) { + return makeLiveLayer({ + cwd: process.cwd(), + debug: options?.debug === true, + nonInteractive: options?.nonInteractive === true, + })(Effect.promise(runLegacy)); +} + +function makeInitCommand() { + return makeCommand( + "init", + { + dir: initDirectoryArg, + appType: optionalChoiceOption("app-type", ["browser", "webviewer"] as const, "The type of app to create"), + ui: optionalChoiceOption("ui", ["shadcn", "mantine"] as const, "The UI scaffold 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 name of the FileMaker 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"), + 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 flags: CliFlags = { + ...defaultCliFlags, + appType: getOrUndefined(options.appType), + ui: getOrUndefined(options.ui), + server: getOrUndefined(options.server), + adminApiKey: getOrUndefined(options.adminApiKey), + fileName: getOrUndefined(options.fileName), + layoutName: getOrUndefined(options.layoutName), + schemaName: getOrUndefined(options.schemaName), + dataApiKey: getOrUndefined(options.dataApiKey), + 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: Boolean(flags.CI || flags.nonInteractive), + })(runInit(getOrUndefined(dir), flags)); + }, + ).pipe(withCommandDescription("Create a new project with ProofKit")); +} + +function makeAddCommand() { + return makeCommand( + "add", + { + name: optionalArg(textArg({ name: "name" })).pipe(withArgDescription("Component or registry item to add")), + 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, 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 }); + }, + { nonInteractive: CI || nonInteractive, debug }, + ), + ).pipe(withCommandDescription("Add a new component to your project")); +} + +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("Remove a component from your project")); +} + +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 }, + ), + ).pipe(withCommandDescription("Generate types for your project")); +} + +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("Upgrade ProofKit components in your project")); +} + +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: Boolean(options.CI || options.nonInteractive), + })( + runDefaultCommand({ + ...defaultCliFlags, + CI: options.CI, + nonInteractive: options.nonInteractive, + debug: options.debug, + }), + ), +).pipe( + withCommandDescription("Interactive CLI to scaffold and manage ProofKit projects"), + withSubcommands([ + makeInitCommand(), + makeAddCommand(), + makeRemoveCommand(), + makeTypegenCommand(), + makeDeployCommand(), + makeUpgradeCommand(), + ]), +); + +export const cli = run(rootCommand, { + name: "ProofKit", + version: getCliVersion(), }); + +const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +const debugFlagNames = new Set(["--debug"]); + +function shouldShowDebugDetails(argv: readonly string[]) { + return argv.some((arg) => debugFlagNames.has(arg)); +} + +function renderFailure(cause: Cause.Cause, showDebugDetails: boolean) { + const failure = getOrUndefined(Cause.failureOption(cause)); + + if (failure && !isValidationError(failure)) { + const error = Cause.squash(cause); + console.error(error instanceof Error ? error.message : String(error)); + } else if (!failure) { + 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(cli(argv).pipe(Effect.provide(nodeContextLayer))); + + if (Exit.isFailure(exit)) { + renderFailure(exit.cause, showDebugDetails); + process.exitCode = 1; + } +} + +if (isMainModule) { + renderTitle(getCliVersion()); + main(process.argv).catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/packages/new/src/services/live.ts b/packages/cli/src/services/live.ts similarity index 100% rename from packages/new/src/services/live.ts rename to packages/cli/src/services/live.ts diff --git a/packages/new/src/utils/browserOpen.ts b/packages/cli/src/utils/browserOpen.ts similarity index 100% rename from packages/new/src/utils/browserOpen.ts rename to packages/cli/src/utils/browserOpen.ts diff --git a/packages/new/src/utils/http.ts b/packages/cli/src/utils/http.ts similarity index 100% rename from packages/new/src/utils/http.ts rename to packages/cli/src/utils/http.ts diff --git a/packages/new/src/utils/packageManager.ts b/packages/cli/src/utils/packageManager.ts similarity index 100% rename from packages/new/src/utils/packageManager.ts rename to packages/cli/src/utils/packageManager.ts diff --git a/packages/new/src/utils/projectFiles.ts b/packages/cli/src/utils/projectFiles.ts similarity index 90% rename from packages/new/src/utils/projectFiles.ts rename to packages/cli/src/utils/projectFiles.ts index fc018bf1..f70dd1e8 100644 --- a/packages/new/src/utils/projectFiles.ts +++ b/packages/cli/src/utils/projectFiles.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import path from "node:path"; -import { fileURLToPath } from "node:url"; import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser"; +import { PKG_ROOT } from "~/consts.js"; import type { FileMakerEnvNames } from "~/core/types.js"; import type { PackageManager } from "~/utils/packageManager.js"; @@ -77,13 +77,13 @@ export async function replaceTextInFiles( const entries = await fs.readdir(rootDir); for (const entry of entries) { const fullPath = path.join(rootDir, entry); - const extension = path.extname(entry); - - if (!(extension || entry.includes("."))) { - await replaceTextInFiles(fs, fullPath, searchValue, replaceValue).catch(() => undefined); + const childEntries = await fs.readdir(fullPath).catch(() => undefined); + if (childEntries) { + await replaceTextInFiles(fs, fullPath, searchValue, replaceValue); continue; } + const extension = path.extname(entry); if (!textFileExtensions.has(extension)) { continue; } @@ -243,21 +243,18 @@ export async function updateTypegenConfig( } export function getScaffoldVersion() { - try { - const packageJsonUrl = new URL("../../package.json", import.meta.url); - const packageJson = JSON.parse(readFileSync(fileURLToPath(packageJsonUrl), "utf8")) as { version?: string }; - if (packageJson.version && packageJson.version !== "0.0.0-private") { - return packageJson.version; + 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 } - } catch { - // ignore } - try { - const cliPackageJsonUrl = new URL("../../../cli/package.json", import.meta.url); - const cliPackageJson = JSON.parse(readFileSync(fileURLToPath(cliPackageJsonUrl), "utf8")) as { version?: string }; - return cliPackageJson.version ?? "0.0.0-private"; - } catch { - return "0.0.0-private"; - } + return "0.0.0-private"; } diff --git a/packages/new/src/utils/projectName.ts b/packages/cli/src/utils/projectName.ts similarity index 100% rename from packages/new/src/utils/projectName.ts rename to packages/cli/src/utils/projectName.ts diff --git a/packages/new/src/utils/prompts.ts b/packages/cli/src/utils/prompts.ts similarity index 100% rename from packages/new/src/utils/prompts.ts rename to packages/cli/src/utils/prompts.ts diff --git a/packages/cli/src/utils/renderTitle.ts b/packages/cli/src/utils/renderTitle.ts index d0f89738..1a6a8898 100644 --- a/packages/cli/src/utils/renderTitle.ts +++ b/packages/cli/src/utils/renderTitle.ts @@ -1,7 +1,6 @@ import gradient from "gradient-string"; - -import { TITLE_TEXT } from "~/consts.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; +import { getTitleText } from "~/consts.js"; +import { detectUserPackageManager } from "~/utils/packageManager.js"; const proofTheme = { purple: "#89216B", @@ -10,11 +9,10 @@ const proofTheme = { }; export const proofGradient = gradient(Object.values(proofTheme)); -export const renderTitle = () => { - // resolves weird behavior where the ascii is offset - const pkgManager = getUserPkgManager(); +export function renderTitle(version = "0.0.0-private") { + const pkgManager = detectUserPackageManager(); if (pkgManager === "yarn" || pkgManager === "pnpm") { console.log(""); } - console.log(proofGradient.multiline(TITLE_TEXT)); -}; + console.log(proofGradient.multiline(getTitleText(version))); +} diff --git a/packages/new/src/utils/versioning.ts b/packages/cli/src/utils/versioning.ts similarity index 100% rename from packages/new/src/utils/versioning.ts rename to packages/cli/src/utils/versioning.ts diff --git a/packages/cli/tests/browser-apps.smoke.test.ts b/packages/cli/tests/browser-apps.smoke.test.ts new file mode 100644 index 00000000..ef7d1c3a --- /dev/null +++ b/packages/cli/tests/browser-apps.smoke.test.ts @@ -0,0 +1,87 @@ +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"; + +describe("External integration smoke tests (non-interactive CLI)", () => { + // 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 = 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), + }) + .parse(process.env); + + 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/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index f86d3fb4..c6b51249 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -1,22 +1,107 @@ -import { execSync } from "node:child_process"; +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"; -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(); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const packageDir = path.join(__dirname, ".."); +const distEntry = path.join(packageDir, "dist/index.js"); + +function buildCli() { + execFileSync("pnpm", ["build"], { + cwd: packageDir, + stdio: "pipe", + encoding: "utf8", }); +} + +describe("proofkit CLI", () => { + it("shows kebab-case init flags in help", () => { + buildCli(); + const output = execFileSync("node", [distEntry, "init", "--help"], { + cwd: packageDir, + stdio: "pipe", + encoding: "utf8", + }); + + expect(output).toContain("--app-type"); + expect(output).toContain("--non-interactive"); + expect(output).toContain("--no-install"); + expect(output).toContain("--no-git"); + expect(output).not.toContain("--appType"); + }); + + it("prints the header and project command guidance when run inside a ProofKit project", async () => { + buildCli(); + 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 add"); + }); + + it("fails with guidance when no command is used in non-interactive mode", () => { + buildCli(); + 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("shows a clean invalid subcommand error by default", () => { + buildCli(); + 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', 'add', 'remove', 'typegen', 'deploy', 'upgrade'", + ); + expect(output).not.toContain('"CommandMismatch"'); + expect(output).not.toContain("[debug]"); + }); + + it("shows internal error details when debug mode is enabled", () => { + buildCli(); + const result = spawnSync("node", [distEntry, "--debug", "my-proofkit-app", "--force"], { + cwd: packageDir, + stdio: "pipe", + encoding: "utf8", + }); + + const output = `${result.stdout}\n${result.stderr}`; - it("should be executable", () => { - expect(() => { - execSync("node ../dist/index.js --version", { - cwd: import.meta.dirname, - encoding: "utf-8", - }); - }).not.toThrow(); + expect(result.status).not.toBe(0); + expect(output).toContain( + "Invalid subcommand for proofkit - use one of 'init', 'add', 'remove', 'typegen', 'deploy', 'upgrade'", + ); + expect(output).toContain("[debug]"); + expect(output).toContain('"CommandMismatch"'); }); }); diff --git a/packages/new/tests/default-command.test.ts b/packages/cli/tests/default-command.test.ts similarity index 91% rename from packages/new/tests/default-command.test.ts rename to packages/cli/tests/default-command.test.ts index 9f2e7ce8..93d391fa 100644 --- a/packages/new/tests/default-command.test.ts +++ b/packages/cli/tests/default-command.test.ts @@ -41,7 +41,7 @@ describe("default command routing", () => { expect(consoleTranscript.note.some((entry) => entry.title === "Coming soon")).toBe(false); }); - it("shows a coming-soon placeholder when a ProofKit project is present", async () => { + it("shows explicit project command guidance when a ProofKit project is present", async () => { const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-project-")); await fs.writeJson(path.join(cwd, "proofkit.json"), { appType: "browser", @@ -65,8 +65,8 @@ describe("default command routing", () => { expect(consoleTranscript.note).toEqual([ { - title: "Coming soon", - message: expect.stringContaining("Project command routing is coming soon"), + title: "Project commands", + message: expect.stringContaining("Use an explicit command such as `proofkit add`"), }, ]); }); diff --git a/packages/new/tests/executor.test.ts b/packages/cli/tests/executor.test.ts similarity index 100% rename from packages/new/tests/executor.test.ts rename to packages/cli/tests/executor.test.ts diff --git a/packages/new/tests/init-fixtures.ts b/packages/cli/tests/init-fixtures.ts similarity index 75% rename from packages/new/tests/init-fixtures.ts rename to packages/cli/tests/init-fixtures.ts index f155d0ae..63551e8e 100644 --- a/packages/new/tests/init-fixtures.ts +++ b/packages/cli/tests/init-fixtures.ts @@ -37,11 +37,17 @@ export async function readScaffoldArtifacts(projectDir: string) { 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 launchPath = path.join(projectDir, ".claude", "launch.json"); 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, + launchConfig: (await fs.pathExists(launchPath)) ? await fs.readFile(launchPath, "utf8") : undefined, }; } diff --git a/packages/cli/tests/init-non-interactive-failures.test.ts b/packages/cli/tests/init-non-interactive-failures.test.ts new file mode 100644 index 00000000..330b3bf5 --- /dev/null +++ b/packages/cli/tests/init-non-interactive-failures.test.ts @@ -0,0 +1,222 @@ +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", "--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("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", + "--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(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("does not surface typegen guidance for browser scaffolds without a typegen script", () => { + 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; + }; + expect(packageJson.scripts?.typegen).toBeUndefined(); + expect(output).not.toMatch(typegenCommandPattern); + }); +}); diff --git a/packages/cli/tests/init-post-init-generation-errors.test.ts b/packages/cli/tests/init-post-init-generation-errors.test.ts new file mode 100644 index 00000000..f13e9dff --- /dev/null +++ b/packages/cli/tests/init-post-init-generation-errors.test.ts @@ -0,0 +1,62 @@ +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/tests/init-run-init-regression.test.ts b/packages/cli/tests/init-run-init-regression.test.ts new file mode 100644 index 00000000..6422d97b --- /dev/null +++ b/packages/cli/tests/init-run-init-regression.test.ts @@ -0,0 +1,197 @@ +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/tests/init-scaffold-contract.test.ts b/packages/cli/tests/init-scaffold-contract.test.ts new file mode 100644 index 00000000..01145813 --- /dev/null +++ b/packages/cli/tests/init-scaffold-contract.test.ts @@ -0,0 +1,228 @@ +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 { + if (!existsSync(cliPath)) { + execFileSync("pnpm", ["build"], { + cwd: join(__dirname, ".."), + env: process.env, + stdio: "pipe", + }); + } + + 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[] { + 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; + fmHttp?: { + 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?.fmHttp?.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/new/tests/integration.test.ts b/packages/cli/tests/integration.test.ts similarity index 70% rename from packages/new/tests/integration.test.ts rename to packages/cli/tests/integration.test.ts index 55cfda34..c3694702 100644 --- a/packages/new/tests/integration.test.ts +++ b/packages/cli/tests/integration.test.ts @@ -63,8 +63,9 @@ describe("integration scaffold generation", () => { expect(packageJson.name).toBe("browser-app"); expect(packageJson.packageManager).toBe("pnpm@10.27.0"); expect(packageJson.proofkitMetadata).toMatchObject({ - scaffoldPackage: "@proofkit/new", + scaffoldPackage: "@proofkit/cli", }); + expect(packageJson.devDependencies["@proofkit/cli"]).toBe("beta"); expect(typeof packageJson.proofkitMetadata?.initVersion).toBe("string"); expect(packageJson.proofkitMetadata?.initVersion).not.toBe(""); expect(proofkitJson).toMatchObject({ @@ -142,6 +143,71 @@ describe("integration scaffold generation", () => { 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, 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"); + + expect(packageJson.scripts.lint).toBe("ultracite check ."); + expect(packageJson.scripts.format).toBe("ultracite fix ."); + 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.8"); + expect(agentsFile).toContain("Use the ProofKit docs as the primary reference"); + expect(agentsFile).toContain("npx @tanstack/intent@latest install"); + expect(claudeFile?.trim()).toBe("AGENTS.md"); + expect(launchConfig).toContain('"runtimeExecutable": "pnpm"'); + expect(routerFile).toContain("createHashHistory"); + expect(mainFile).toContain("QueryClientProvider"); + expect(queryDemoFile).toContain("TanStack Query is preconfigured"); + const nextStepsMessage = consoleTranscript.info.at(-1) ?? ""; + expect(nextStepsMessage).toContain("Have your agent run this in the new project"); + expect(nextStepsMessage).toContain("complete the interactive prompt"); + 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({ diff --git a/packages/new/tests/planner.test.ts b/packages/cli/tests/planner.test.ts similarity index 97% rename from packages/new/tests/planner.test.ts rename to packages/cli/tests/planner.test.ts index f95f5a4f..73353dd9 100644 --- a/packages/new/tests/planner.test.ts +++ b/packages/cli/tests/planner.test.ts @@ -14,6 +14,7 @@ describe("planInit", () => { expect(plan.templateDir).toBe("/templates/browser"); expect(plan.packageJson.name).toBe("demo-app"); expect(plan.settings.appType).toBe("browser"); + expect(plan.packageJson.devDependencies["@proofkit/cli"]).toBe("beta"); expect(plan.tasks.runInstall).toBe(true); expect(plan.tasks.initializeGit).toBe(true); expect(plan.tasks.bootstrapFileMaker).toBe(false); diff --git a/packages/new/tests/project-name.test.ts b/packages/cli/tests/project-name.test.ts similarity index 100% rename from packages/new/tests/project-name.test.ts rename to packages/cli/tests/project-name.test.ts diff --git a/packages/new/tests/prompts.test.ts b/packages/cli/tests/prompts.test.ts similarity index 100% rename from packages/new/tests/prompts.test.ts rename to packages/cli/tests/prompts.test.ts diff --git a/packages/new/tests/resolve-init.test.ts b/packages/cli/tests/resolve-init.test.ts similarity index 100% rename from packages/new/tests/resolve-init.test.ts rename to packages/cli/tests/resolve-init.test.ts diff --git a/packages/new/tests/test-layer.ts b/packages/cli/tests/test-layer.ts similarity index 100% rename from packages/new/tests/test-layer.ts rename to packages/cli/tests/test-layer.ts diff --git a/packages/cli/tests/test-utils.ts b/packages/cli/tests/test-utils.ts index e673c887..7c8ce0b9 100644 --- a/packages/cli/tests/test-utils.ts +++ b/packages/cli/tests/test-utils.ts @@ -3,10 +3,10 @@ import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; /** - * Replaces @proofkit/* package versions with "latest" in package.json - * This fixes CI issues where scaffolded projects reference unpublished versions + * Smoke-test helper only: swap workspace refs to published tags so install/build + * validates what end users can actually fetch from the registry. */ -function useLatestProofkitVersions(projectDir: string): void { +function usePublishedProofkitVersionsForSmoke(projectDir: string): void { const pkgPath = join(projectDir, "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); @@ -34,12 +34,12 @@ function useLatestProofkitVersions(projectDir: string): void { * @param projectDir The directory containing the project to build * @throws If the build fails */ -export function verifyProjectBuilds(projectDir: string): void { +export function verifySmokeProjectBuilds(projectDir: string): void { console.log(`\nVerifying project build in ${projectDir}...`); try { - // Replace unpublished @proofkit versions with latest - useLatestProofkitVersions(projectDir); + // Smoke tests intentionally validate published package installability. + usePublishedProofkitVersionsForSmoke(projectDir); console.log("Installing dependencies..."); // Run pnpm install while ignoring workspace settings diff --git a/packages/cli/tests/webviewer-apps.test.ts b/packages/cli/tests/webviewer-apps.test.ts index 831766c2..f97c80c1 100644 --- a/packages/cli/tests/webviewer-apps.test.ts +++ b/packages/cli/tests/webviewer-apps.test.ts @@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { execFileSync, 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"; @@ -12,6 +12,13 @@ describe("WebViewer CLI Tests", () => { const projectDir = join(testDir, projectName); beforeEach(() => { + if (!existsSync(cliPath)) { + execFileSync("pnpm", ["build"], { + cwd: join(__dirname, ".."), + env: process.env, + stdio: "pipe", + }); + } if (existsSync(projectDir)) { rmSync(projectDir, { recursive: true, force: true }); } @@ -23,9 +30,9 @@ describe("WebViewer CLI Tests", () => { `node "${cliPath}" init`, projectName, "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", + "--app-type webviewer", + "--no-git", + "--no-install", ].join(" "); expect(() => { @@ -60,9 +67,9 @@ describe("WebViewer CLI Tests", () => { `node "${cliPath}" init`, projectName, "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", + "--app-type webviewer", + "--no-git", + "--no-install", ].join(" "); expect(() => { @@ -86,9 +93,9 @@ describe("WebViewer CLI Tests", () => { `node "${cliPath}" init`, projectName, "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", + "--app-type webviewer", + "--no-git", + "--no-install", ].join(" "); expect(() => { @@ -111,9 +118,9 @@ describe("WebViewer CLI Tests", () => { `node "${cliPath}" init`, projectName, "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", + "--app-type webviewer", + "--no-git", + "--no-install", ].join(" "); expect(() => { @@ -136,9 +143,9 @@ describe("WebViewer CLI Tests", () => { `node "${cliPath}" init`, projectName, "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", + "--app-type webviewer", + "--no-git", + "--no-install", ].join(" "); expect(() => { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 5e73ded4..9d98475f 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,12 +3,10 @@ "compilerOptions": { "baseUrl": "./", "paths": { - "~/*": ["./src/*"], - "@config/*": ["../config/*"] + "~/*": ["./src/*"] }, - "checkJs": true, "strictNullChecks": true }, - "exclude": ["template"], - "include": ["src", "tsdown.config.ts", "../reset.d.ts", "index.d.ts"] + "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 index c9df787c..0cb69d19 100644 --- a/packages/cli/tsdown.config.ts +++ b/packages/cli/tsdown.config.ts @@ -1,31 +1,7 @@ -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"], @@ -33,22 +9,5 @@ export default defineConfig({ 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 ? "http://localhost:3005" : "https://proofkit.dev"), - }, - }), - ], - onSuccess: isDev ? "node dist/index.js" : undefined, }); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index c5b48b63..e22c9529 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -10,15 +10,9 @@ export default defineConfig({ test: { globals: true, environment: "node", - setupFiles: ["./tests/setup.ts"], include: ["tests/**/*.test.ts"], - // Exclude E2E tests that require real credentials - exclude: ["**/node_modules/**", "**/dist/**", "tests/browser-apps.test.ts"], - testTimeout: 60_000, // 60 seconds for CLI tests which can be slow - coverage: { - provider: "v8", - reporter: ["text", "json", "html"], - include: ["src/**/*.ts"], - }, + exclude: ["**/node_modules/**", "**/dist/**", "tests/**/*.smoke.test.ts"], + fileParallelism: false, + testTimeout: 60_000, }, }); diff --git a/packages/new/vitest.config.ts b/packages/cli/vitest.smoke.config.ts similarity index 84% rename from packages/new/vitest.config.ts rename to packages/cli/vitest.smoke.config.ts index 68059ee9..6cd6e29e 100644 --- a/packages/new/vitest.config.ts +++ b/packages/cli/vitest.smoke.config.ts @@ -13,7 +13,8 @@ export default defineConfig({ test: { globals: true, environment: "node", - include: ["tests/**/*.test.ts"], + setupFiles: ["./tests/setup.ts"], + include: ["tests/**/*.smoke.test.ts"], exclude: ["**/node_modules/**", "**/dist/**"], testTimeout: 60_000, }, diff --git a/packages/create-proofkit/package.json b/packages/create-proofkit/package.json index f0530a0e..450a5e51 100644 --- a/packages/create-proofkit/package.json +++ b/packages/create-proofkit/package.json @@ -17,11 +17,15 @@ "pub:release": "npm publish --access public", "pub:beta": "npm publish --tag beta --access public", "lint": "biome check . --write", - "lint:summary": "biome check . --reporter=summary" + "lint:summary": "biome check . --reporter=summary", + "test": "vitest run --config vitest.config.ts" }, "dependencies": { "execa": "^9.6.1" }, + "devDependencies": { + "vitest": "^4.0.17" + }, "engines": { "node": ">=20.12.0" }, diff --git a/packages/create-proofkit/src/index.js b/packages/create-proofkit/src/index.js index c22df252..125b97d9 100644 --- a/packages/create-proofkit/src/index.js +++ b/packages/create-proofkit/src/index.js @@ -1,8 +1,19 @@ #!/usr/bin/env node import { execa } from "execa"; +import { createRequire } from "node:module"; 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); @@ -19,7 +30,7 @@ async function main() { } try { - await execa(pkgManagerCmd, ["@proofkit/cli@latest", "init", ...args], { + await execa(pkgManagerCmd, [getCliSpecifier(), "init", ...args], { stdio: "inherit", env: { ...process.env, diff --git a/packages/create-proofkit/tests/index.test.js b/packages/create-proofkit/tests/index.test.js new file mode 100644 index 00000000..0b7da027 --- /dev/null +++ b/packages/create-proofkit/tests/index.test.js @@ -0,0 +1,104 @@ +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 new file mode 100644 index 00000000..f12e4e8a --- /dev/null +++ b/packages/create-proofkit/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.js"], + }, +}); diff --git a/packages/new/package.json b/packages/new/package.json deleted file mode 100644 index 6851ea3d..00000000 --- a/packages/new/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "@proofkit/new", - "version": "0.0.0-private", - "private": true, - "description": "Internal scaffold package for the next ProofKit CLI", - "license": "MIT", - "type": "module", - "exports": { - ".": { - "import": "./dist/index.js" - } - }, - "bin": { - "proofkit-new": "dist/index.js" - }, - "files": [ - "dist", - "README.md", - "package.json" - ], - "engines": { - "node": "^20.0.0 || ^22.0.0" - }, - "scripts": { - "typecheck": "tsc", - "build": "NODE_ENV=production tsdown", - "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", - "test": "vitest run" - }, - "dependencies": { - "@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", - "axios": "^1.13.2", - "chalk": "5.4.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", - "ora": "6.3.1", - "sort-package-json": "^2.15.1", - "type-fest": "^3.13.1" - }, - "devDependencies": { - "@biomejs/biome": "2.3.11", - "@types/fs-extra": "^11.0.4", - "@types/gradient-string": "^1.1.6", - "@types/node": "^22.19.5", - "@vitest/coverage-v8": "^2.1.9", - "tsdown": "^0.14.2", - "typescript": "^5.9.3", - "ultracite": "7.0.8", - "vitest": "^4.0.17" - } -} diff --git a/packages/new/src/utils/renderTitle.ts b/packages/new/src/utils/renderTitle.ts deleted file mode 100644 index 7f1186c4..00000000 --- a/packages/new/src/utils/renderTitle.ts +++ /dev/null @@ -1,19 +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 packageManager = detectUserPackageManager(); - if (packageManager === "yarn" || packageManager === "pnpm") { - console.log(""); - } - console.log(proofGradient.multiline(getTitleText(version))); -} diff --git a/packages/new/tests/cli.test.ts b/packages/new/tests/cli.test.ts deleted file mode 100644 index f4d634d2..00000000 --- a/packages/new/tests/cli.test.ts +++ /dev/null @@ -1,70 +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, "dist/index.js"); - -function buildCli() { - execFileSync("pnpm", ["build"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); -} - -describe("proofkit-new CLI", () => { - it("shows kebab-case init flags in help", () => { - buildCli(); - const output = execFileSync("node", [distEntry, "init", "--help"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(output).toContain("--app-type"); - expect(output).toContain("--non-interactive"); - expect(output).toContain("--no-install"); - expect(output).toContain("--no-git"); - expect(output).not.toContain("--appType"); - }); - - it("prints the header and coming-soon message when run inside a ProofKit project", async () => { - buildCli(); - 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("Coming soon"); - }); - - it("fails with guidance when no command is used in non-interactive mode", () => { - buildCli(); - 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-new init --non-interactive"); - }); -}); diff --git a/packages/new/tsconfig.json b/packages/new/tsconfig.json deleted file mode 100644 index 9d98475f..00000000 --- a/packages/new/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": "./", - "paths": { - "~/*": ["./src/*"] - }, - "strictNullChecks": true - }, - "exclude": ["template", "dist"], - "include": ["src", "tests", "tsdown.config.ts", "vitest.config.ts"] -} diff --git a/packages/new/tsdown.config.ts b/packages/new/tsdown.config.ts deleted file mode 100644 index 07586488..00000000 --- a/packages/new/tsdown.config.ts +++ /dev/null @@ -1,14 +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, - onSuccess: isDev ? "node dist/index.js" : undefined, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c78b53d2..1990d2f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,19 +92,19 @@ importers: version: 2.1.1 fumadocs-core: specifier: 16.4.4 - version: 16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + version: 16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) fumadocs-mdx: specifier: 14.2.4 - version: 14.2.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@6.4.1(@types/node@22.19.5)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) + version: 14.2.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@6.4.1(@types/node@22.19.5)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) fumadocs-twoslash: specifier: ^3.1.12 - version: 3.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + version: 3.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) fumadocs-typescript: specifier: ^5.0.1 - version: 5.0.1(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3) + version: 5.0.1(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3) fumadocs-ui: specifier: 16.4.4 - version: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + version: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) hono: specifier: ^4.11.3 version: 4.11.3 @@ -260,6 +260,21 @@ importers: '@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) @@ -284,6 +299,9 @@ importers: dotenv: specifier: ^16.6.1 version: 16.6.1 + effect: + specifier: ^3.20.0 + version: 3.20.0 es-toolkit: specifier: ^1.43.0 version: 1.43.0 @@ -332,6 +350,9 @@ importers: ts-morph: specifier: ^26.0.0 version: 26.0.0 + type-fest: + specifier: ^3.13.1 + version: 3.13.1 devDependencies: '@auth/drizzle-adapter': specifier: ^1.11.1 @@ -371,7 +392,212 @@ importers: 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(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + 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(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(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/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 + '@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(@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)) + 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)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/react@19.2.7)(better-sqlite3@11.10.0)(kysely@0.28.12)(mysql2@3.16.0)(postgres@3.4.8)(react@19.2.3) + mysql2: + specifier: ^3.16.0 + version: 3.16.0 + next: + specifier: 16.1.1 + version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next-auth: + specifier: ^4.24.13 + version: 4.24.13(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + 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 + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + superjson: + specifier: ^2.2.6 + version: 2.2.6 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + tsdown: + specifier: ^0.14.2 + version: 0.14.2(oxc-resolver@11.16.2)(publint@0.3.16)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + ultracite: + specifier: 7.0.8 + version: 7.0.8(effect@3.20.0)(typescript@5.9.3) + vitest: + specifier: ^4.0.17 + version: 4.0.17(@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) + zod: + specifier: ^4.3.5 + version: 4.3.5 + + packages/cli-old: + dependencies: + '@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 + '@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/glob': + specifier: ^8.1.0 + version: 8.1.0 + 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 + 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 + open: + specifier: ^10.2.0 + version: 10.2.0 + ora: + specifier: 6.3.1 + version: 6.3.1 + randomstring: + specifier: ^1.3.1 + version: 1.3.1 + 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) + sort-package-json: + specifier: ^2.15.1 + version: 2.15.1 + ts-morph: + specifier: ^26.0.0 + version: 26.0.0 + 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)) + '@biomejs/biome': + specifier: 2.3.11 + version: 2.3.11 + '@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/registry': + specifier: workspace:* + version: link:../registry + '@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.6) + '@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(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(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) @@ -416,7 +642,7 @@ importers: version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: ^4.24.13 - version: 4.24.13(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.24.13(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) postgres: specifier: ^3.4.8 version: 3.4.8 @@ -455,13 +681,17 @@ importers: version: 4.0.17(@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) zod: specifier: ^4.3.5 - version: 4.3.5 + version: 4.3.6 packages/create-proofkit: dependencies: execa: specifier: ^9.6.1 version: 9.6.1 + devDependencies: + vitest: + specifier: ^4.0.17 + version: 4.0.17(@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: @@ -585,91 +815,6 @@ importers: specifier: ^4.3.5 version: 4.3.5 - packages/new: - dependencies: - '@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) - axios: - specifier: ^1.13.2 - version: 1.13.2 - chalk: - specifier: 5.4.1 - version: 5.4.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 - ora: - specifier: 6.3.1 - version: 6.3.1 - sort-package-json: - specifier: ^2.15.1 - version: 2.15.1 - type-fest: - specifier: ^3.13.1 - version: 3.13.1 - devDependencies: - '@biomejs/biome': - specifier: 2.3.11 - version: 2.3.11 - '@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 - '@vitest/coverage-v8': - specifier: ^2.1.9 - version: 2.1.9(vitest@4.0.17(@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)) - tsdown: - specifier: ^0.14.2 - version: 0.14.2(oxc-resolver@11.16.2)(publint@0.3.16)(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - ultracite: - specifier: 7.0.8 - version: 7.0.8(effect@3.20.0)(typescript@5.9.3) - vitest: - specifier: ^4.0.17 - version: 4.0.17(@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) - packages/registry: dependencies: jiti: @@ -2801,8 +2946,8 @@ packages: resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} engines: {node: '>= 20.0.0'} - '@oxc-project/types@0.115.0': - resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@oxc-project/types@0.120.0': + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} '@oxc-resolver/binding-android-arm-eabi@11.16.2': resolution: {integrity: sha512-lVJbvydLQIDZHKUb6Zs9Rq80QVTQ9xdCQE30eC9/cjg4wsMoEOg65QZPymUAIVJotpUAWJD0XYcwE7ugfxx5kQ==} @@ -3805,91 +3950,91 @@ packages: peerDependencies: react: '>=18.2.0' - '@rolldown/binding-android-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} + '@rolldown/binding-android-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.9': - resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} + '@rolldown/binding-darwin-x64@1.0.0-rc.10': + resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': - resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': - resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': - resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3897,8 +4042,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rolldown/pluginutils@1.0.0-rc.9': - resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} + '@rolldown/pluginutils@1.0.0-rc.10': + resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} '@rollup/plugin-replace@6.0.3': resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} @@ -4949,10 +5094,6 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -7604,8 +7745,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-rc.9: - resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} + rolldown@1.0.0-rc.10: + resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -9692,9 +9833,9 @@ snapshots: '@formatjs/fast-memoize': 3.0.3 tslib: 2.8.1 - '@fumadocs/ui@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5)': + '@fumadocs/ui@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5)': dependencies: - fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) postcss-selector-parser: 7.1.1 react: 19.2.3 @@ -10386,7 +10527,7 @@ snapshots: '@orama/orama@3.1.18': {} - '@oxc-project/types@0.115.0': {} + '@oxc-project/types@0.120.0': {} '@oxc-resolver/binding-android-arm-eabi@11.16.2': optional: true @@ -11356,56 +11497,56 @@ snapshots: dependencies: react: 19.2.3 - '@rolldown/binding-android-arm64@1.0.0-rc.9': + '@rolldown/binding-android-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.9': + '@rolldown/binding-darwin-x64@1.0.0-rc.10': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': optional: true '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rolldown/pluginutils@1.0.0-rc.9': {} + '@rolldown/pluginutils@1.0.0-rc.10': {} '@rollup/plugin-replace@6.0.3(rollup@4.55.1)': dependencies: @@ -11684,6 +11825,12 @@ snapshots: optionalDependencies: typescript: 5.9.3 + '@t3-oss/env-core@0.10.1(typescript@5.9.3)(zod@4.3.6)': + dependencies: + zod: 4.3.6 + 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) @@ -11691,6 +11838,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 + '@t3-oss/env-nextjs@0.10.1(typescript@5.9.3)(zod@4.3.6)': + dependencies: + '@t3-oss/env-core': 0.10.1(typescript@5.9.3)(zod@4.3.6) + zod: 4.3.6 + optionalDependencies: + typescript: 5.9.3 + '@tabler/icons-react@3.36.1(react@19.2.3)': dependencies: '@tabler/icons': 3.36.1 @@ -11839,7 +11993,7 @@ snapshots: 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(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@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(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(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 @@ -11978,7 +12132,7 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 25.0.6 + '@types/node': 22.19.5 '@types/mdast@4.0.4': dependencies: @@ -12099,24 +12253,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.9(vitest@4.0.17(@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))': - 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(@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) - transitivePeerDependencies: - - supports-color - '@vitest/coverage-v8@2.1.9(vitest@4.0.17(@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 @@ -12581,8 +12717,6 @@ snapshots: chalk@5.4.1: {} - chalk@5.6.2: {} - char-regex@1.0.2: {} character-entities-html4@2.1.0: {} @@ -13464,7 +13598,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5): + fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5): dependencies: '@formatjs/intl-localematcher': 0.7.5 '@orama/orama': 3.1.18 @@ -13495,7 +13629,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5): + fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5): dependencies: '@formatjs/intl-localematcher': 0.7.5 '@orama/orama': 3.1.18 @@ -13526,14 +13660,14 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@6.4.1(@types/node@22.19.5)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): + fumadocs-mdx@14.2.4(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@6.4.1(@types/node@22.19.5)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.27.2 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) js-yaml: 4.1.1 mdast-util-to-markdown: 2.1.2 picocolors: 1.1.1 @@ -13554,11 +13688,11 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-twoslash@3.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + fumadocs-twoslash@3.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@shikijs/twoslash': 3.21.0(typescript@5.9.3) - fumadocs-ui: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + fumadocs-ui: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) mdast-util-from-markdown: 2.0.2 mdast-util-gfm: 3.1.0 mdast-util-to-hast: 13.2.1 @@ -13574,10 +13708,10 @@ snapshots: - supports-color - typescript - fumadocs-typescript@5.0.1(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3): + fumadocs-typescript@5.0.1(@types/react@19.2.7)(fumadocs-core@16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5))(fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3): dependencies: estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.511.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) hast-util-to-estree: 3.1.3 hast-util-to-jsx-runtime: 2.3.6 react: 19.2.3 @@ -13589,13 +13723,13 @@ snapshots: unist-util-visit: 5.0.0 optionalDependencies: '@types/react': 19.2.7 - fumadocs-ui: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + fumadocs-ui: 16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) transitivePeerDependencies: - supports-color - fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5): + fumadocs-ui@16.4.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5): dependencies: - '@fumadocs/ui': 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) + '@fumadocs/ui': 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.5) '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -13607,7 +13741,7 @@ snapshots: '@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.3) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) class-variance-authority: 0.7.1 - fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) + fumadocs-core: 16.4.4(@types/react@19.2.7)(lucide-react@0.562.0(react@19.2.3))(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.5) lucide-react: 0.562.0(react@19.2.3) next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -14114,7 +14248,7 @@ snapshots: smol-toml: 1.6.0 strip-json-comments: 5.0.3 typescript: 5.9.3 - zod: 4.3.5 + zod: 4.3.6 kolorist@1.8.0: {} @@ -14300,7 +14434,7 @@ snapshots: dependencies: ansi-escapes: 7.2.0 ansi-regex: 6.2.2 - chalk: 5.6.2 + chalk: 5.4.1 cli-highlight: 2.1.11 cli-table3: 0.6.5 marked: 9.1.6 @@ -14954,7 +15088,7 @@ snapshots: neo-async@2.6.2: {} - next-auth@4.24.13(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-auth@4.24.13(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 '@panva/hkdf': 1.2.1 @@ -15709,7 +15843,7 @@ snapshots: rfdc@1.4.1: {} - rolldown-plugin-dts@0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.0-rc.9)(typescript@5.9.3): + rolldown-plugin-dts@0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.0-rc.10)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -15719,33 +15853,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.0-rc.9 + rolldown: 1.0.0-rc.10 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.0-rc.9: + rolldown@1.0.0-rc.10: dependencies: - '@oxc-project/types': 0.115.0 - '@rolldown/pluginutils': 1.0.0-rc.9 + '@oxc-project/types': 0.120.0 + '@rolldown/pluginutils': 1.0.0-rc.10 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-x64': 1.0.0-rc.9 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + '@rolldown/binding-android-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-x64': 1.0.0-rc.10 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 rollup-plugin-preserve-directives@0.4.0(rollup@4.55.1): dependencies: @@ -16292,13 +16426,13 @@ snapshots: trough@2.2.0: {} - trpc-cli@0.12.2(@trpc/server@11.8.1(typescript@5.9.3))(effect@3.20.0)(zod@4.3.5): + trpc-cli@0.12.2(@trpc/server@11.8.1(typescript@5.9.3))(effect@3.20.0)(zod@4.3.6): dependencies: commander: 14.0.2 optionalDependencies: '@trpc/server': 11.8.1(typescript@5.9.3) effect: 3.20.0 - zod: 4.3.5 + zod: 4.3.6 ts-morph@18.0.0: dependencies: @@ -16336,8 +16470,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-rc.9 - rolldown-plugin-dts: 0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.0-rc.9)(typescript@5.9.3) + rolldown: 1.0.0-rc.10 + rolldown-plugin-dts: 0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.0-rc.10)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.15 @@ -16441,8 +16575,8 @@ snapshots: jsonc-parser: 3.3.1 nypm: 0.6.2 oxlint: 1.39.0 - trpc-cli: 0.12.2(@trpc/server@11.8.1(typescript@5.9.3))(effect@3.20.0)(zod@4.3.5) - zod: 4.3.5 + trpc-cli: 0.12.2(@trpc/server@11.8.1(typescript@5.9.3))(effect@3.20.0)(zod@4.3.6) + zod: 4.3.6 transitivePeerDependencies: - '@orpc/server' - '@valibot/to-json-schema' From 160f8467ef6d9086322136b762632f031c6f8ade Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:43:20 -0500 Subject: [PATCH 02/14] fix lint: formatting and import order Co-Authored-By: Claude Opus 4.6 (1M context) --- biome.json | 7 ++++++- btca.config.jsonc | 2 +- packages/create-proofkit/src/index.js | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/biome.json b/biome.json index 282f819d..6a358dca 100644 --- a/biome.json +++ b/biome.json @@ -62,7 +62,12 @@ "arrowParentheses": "always", "quoteStyle": "double" }, - "globals": ["__FMDAPI_VERSION__", "__BETTER_AUTH_VERSION__", "__WEBVIEWER_VERSION__", "__TYPEGEN_VERSION__"] + "globals": [ + "__FMDAPI_VERSION__", + "__BETTER_AUTH_VERSION__", + "__WEBVIEWER_VERSION__", + "__TYPEGEN_VERSION__" + ] }, "json": { "formatter": { diff --git a/btca.config.jsonc b/btca.config.jsonc index 2543b4b0..7565d307 100644 --- a/btca.config.jsonc +++ b/btca.config.jsonc @@ -10,4 +10,4 @@ ], "model": "gpt-5.3-codex", "provider": "openai" -} \ No newline at end of file +} diff --git a/packages/create-proofkit/src/index.js b/packages/create-proofkit/src/index.js index 125b97d9..3fe032c0 100644 --- a/packages/create-proofkit/src/index.js +++ b/packages/create-proofkit/src/index.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { execa } from "execa"; import { createRequire } from "node:module"; +import { execa } from "execa"; import { getUserPkgManager } from "./getUserPkgManager.js"; const require = createRequire(import.meta.url); From 1f33a8539bbdeebb72fd5ba0e3d34630e993c3ea Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:45:38 -0500 Subject: [PATCH 03/14] fix fmodata validation error cause semantics --- packages/fmodata/src/errors.ts | 8 ++++++-- packages/fmodata/tests/errors.test.ts | 14 +++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/fmodata/src/errors.ts b/packages/fmodata/src/errors.ts index 5c5359cb..987097e6 100644 --- a/packages/fmodata/src/errors.ts +++ b/packages/fmodata/src/errors.ts @@ -112,8 +112,12 @@ export class ValidationError extends FMODataError { cause?: Error["cause"]; }, ) { - const cause = options?.cause ?? issues; - super(message, { cause }); + const cause = options?.cause; + if (cause === undefined) { + super(message); + } else { + super(message, { cause }); + } this.field = options?.field; this.issues = issues; this.value = options?.value; diff --git a/packages/fmodata/tests/errors.test.ts b/packages/fmodata/tests/errors.test.ts index 8c6b4b23..c3aa06d0 100644 --- a/packages/fmodata/tests/errors.test.ts +++ b/packages/fmodata/tests/errors.test.ts @@ -28,7 +28,7 @@ import { ValidationError, } from "@proofkit/fmodata"; import { MockFMServerConnection } from "@proofkit/fmodata/testing"; -import { assert, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { z } from "zod/v4"; describe("Error Handling", () => { @@ -223,7 +223,7 @@ describe("Error Handling", () => { expect(validationError.value).toBeDefined(); }); - it("should preserve Standard Schema issues in cause property", async () => { + it("should preserve Standard Schema issues on the error instance without setting cause", async () => { const invalidData = [ { id: "1", @@ -245,13 +245,9 @@ describe("Error Handling", () => { expect(result.error).toBeInstanceOf(ValidationError); const validationError = result.error as ValidationError; - // The cause property (ES2022 Error.cause) contains the Standard Schema issues array - // This follows the same pattern as uploadthing and is validator-agnostic - assert(validationError.cause, "Cause is not defined"); - - // The cause should be the Standard Schema issues array - expect(Array.isArray(validationError.cause)).toBe(true); - expect(validationError.cause).toBe(validationError.issues); + expect(Array.isArray(validationError.issues)).toBe(true); + expect(validationError.issues.length).toBeGreaterThan(0); + expect(validationError.cause).toBeUndefined(); // The issues array is always available expect(validationError.issues).toBeDefined(); From a9a82e366d31971663c534a5b4379da17573b9e2 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:46:38 -0500 Subject: [PATCH 04/14] Refine docs formatting and exclude cli-old workspace --- apps/docs/src/app/(home)/page.tsx | 17 ++++++++++++++--- apps/docs/src/components/AgentCommand.tsx | 16 ++++++---------- pnpm-workspace.yaml | 1 + 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/docs/src/app/(home)/page.tsx b/apps/docs/src/app/(home)/page.tsx index 2ce6b4ae..77779c92 100644 --- a/apps/docs/src/app/(home)/page.tsx +++ b/apps/docs/src/app/(home)/page.tsx @@ -1,5 +1,16 @@ import { Card, Cards } from "fumadocs-ui/components/card"; -import { Bot, Brain, Code, Database, Globe, RectangleEllipsis, ShieldCheck, Terminal, WandSparkles, WebhookIcon } from "lucide-react"; +import { + Bot, + Brain, + Code, + Database, + Globe, + RectangleEllipsis, + ShieldCheck, + Terminal, + WandSparkles, + WebhookIcon, +} from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import ProofKitLogo from "@/../public/proofkit.png"; @@ -150,8 +161,8 @@ export default function HomePage() { Works with any agent

- Skills are bundled with each package — just install and your AI coding tool picks them up automatically. - Compatible with Claude Code, Cursor, Windsurf, and more. + Skills are bundled with each package — just install and your AI coding tool picks them up + automatically. Compatible with Claude Code, Cursor, Windsurf, and more.

diff --git a/apps/docs/src/components/AgentCommand.tsx b/apps/docs/src/components/AgentCommand.tsx index 007703fb..8e1cea4e 100644 --- a/apps/docs/src/components/AgentCommand.tsx +++ b/apps/docs/src/components/AgentCommand.tsx @@ -23,19 +23,15 @@ export function AgentCommand({ return (
-
- {label} +
+ {label}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 13cf730d..d3279438 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - apps/* - packages/* + - '!packages/cli-old' - '!packages/tmp' - packages/typegen/web From 03d2a927b225ffcfcd902a3fc384ab452b1d30dd Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:57:15 -0500 Subject: [PATCH 05/14] Add direct wrapper tests for create-proofkit Agent-Id: agent-b0866404-cae3-459d-a9c5-f749470d81b4 --- packages/cli/src/consts.ts | 2 +- packages/create-proofkit/src/index.js | 1 + packages/new/src/consts.ts | 39 ----- packages/new/src/index.ts | 214 -------------------------- 4 files changed, 2 insertions(+), 254 deletions(-) delete mode 100644 packages/new/src/consts.ts delete mode 100644 packages/new/src/index.ts diff --git a/packages/cli/src/consts.ts b/packages/cli/src/consts.ts index 6f544052..306c005c 100644 --- a/packages/cli/src/consts.ts +++ b/packages/cli/src/consts.ts @@ -16,6 +16,7 @@ export const AGENT_INSTRUCTIONS = [ "Before doing any AI-assisted development here, run `npx @tanstack/intent@latest install` in the project root to load skills relevant to this project", ].join("\n"); +// 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.dev"; @@ -33,7 +34,6 @@ export function getTitleText(version: string) { 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; diff --git a/packages/create-proofkit/src/index.js b/packages/create-proofkit/src/index.js index 3fe032c0..89ad74ad 100644 --- a/packages/create-proofkit/src/index.js +++ b/packages/create-proofkit/src/index.js @@ -2,6 +2,7 @@ import { createRequire } from "node:module"; import { execa } from "execa"; +import { createRequire } from "node:module"; import { getUserPkgManager } from "./getUserPkgManager.js"; const require = createRequire(import.meta.url); diff --git a/packages/new/src/consts.ts b/packages/new/src/consts.ts deleted file mode 100644 index 2f5676a5..00000000 --- a/packages/new/src/consts.ts +++ /dev/null @@ -1,39 +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 = path.join(distPath, "../"); - -export const DEFAULT_APP_NAME = "my-proofkit-app"; -export const cliName = "proofkit-new"; -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; - } - } - - throw new Error(`Could not locate a template directory. Checked: ${candidates.join(", ")}`); -} - -export const TEMPLATE_ROOT = resolveTemplateRoot(); diff --git a/packages/new/src/index.ts b/packages/new/src/index.ts deleted file mode 100644 index 36a47774..00000000 --- a/packages/new/src/index.ts +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env node -import { readFileSync } 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 { layer as nodeContextLayer } from "@effect/platform-node/NodeContext"; -import { runMain } from "@effect/platform-node/NodeRuntime"; -import { Effect } from "effect"; -import { getOrUndefined } from "effect/Option"; -import { cliName } from "~/consts.js"; -import { - CliContext, - ConsoleService, - FileSystemService, - PackageManagerService, - TemplateService, -} from "~/core/context.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 { makeLiveLayer } from "~/services/live.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() { - try { - const packageJsonUrl = new URL("../package.json", import.meta.url); - const packageJson = JSON.parse(readFileSync(fileURLToPath(packageJsonUrl), "utf8")) as { version?: string }; - return packageJson.version ?? "0.0.0-private"; - } catch { - return "0.0.0-private"; - } -} - -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 packageManagerVersion = yield* Effect.promise(() => - packageManagerService.getVersion(request.packageManager, request.cwd), - ); - const plan = planInit(request, { templateDir, packageManagerVersion }); - yield* executeInitPlan(plan); - return { request, plan }; - }); - -export const runDefaultCommand = (rawFlags?: Partial) => - Effect.gen(function* () { - const cliContext = yield* CliContext; - const fs = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const flags = { ...defaultCliFlags, ...rawFlags }; - - if (cliContext.nonInteractive || flags.CI || flags.nonInteractive) { - return yield* Effect.fail( - new Error( - "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit-new init --non-interactive`.", - ), - ); - } - - const settingsPath = path.join(cliContext.cwd, "proofkit.json"); - const hasProofKitProject = yield* Effect.promise(() => fs.exists(settingsPath)); - - if (hasProofKitProject) { - intro(`Found ${proofGradient("ProofKit")} project`); - consoleService.note( - [ - "Project command routing is coming soon in this new CLI.", - "For now, the available explicit command is `proofkit-new init `.", - ].join("\n"), - "Coming soon", - ); - return; - } - - 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"), -); - -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 makeInitCommand() { - return makeCommand( - "init", - { - dir: initDirectoryArg, - appType: optionalChoiceOption("app-type", ["browser", "webviewer"] as const, "The type of app to create"), - ui: optionalChoiceOption("ui", ["shadcn", "mantine"] as const, "The UI scaffold 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 name of the FileMaker 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"), - 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 flags: CliFlags = { - ...defaultCliFlags, - appType: getOrUndefined(options.appType), - ui: getOrUndefined(options.ui), - server: getOrUndefined(options.server), - adminApiKey: getOrUndefined(options.adminApiKey), - fileName: getOrUndefined(options.fileName), - layoutName: getOrUndefined(options.layoutName), - schemaName: getOrUndefined(options.schemaName), - dataApiKey: getOrUndefined(options.dataApiKey), - 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: Boolean(flags.CI || flags.nonInteractive), - })(runInit(getOrUndefined(dir), flags)); - }, - ).pipe(withCommandDescription("Create a new project with the next ProofKit scaffold")); -} - -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: Boolean(options.CI || options.nonInteractive), - })( - runDefaultCommand({ - ...defaultCliFlags, - CI: options.CI, - nonInteractive: options.nonInteractive, - debug: options.debug, - }), - ), -).pipe( - withCommandDescription("Internal scaffold package for the next ProofKit CLI"), - withSubcommands([makeInitCommand()]), -); - -export const cli = run(rootCommand, { - name: "ProofKit New", - version: getCliVersion(), -}); - -const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); - -if (isMainModule) { - renderTitle(getCliVersion()); - runMain(cli(process.argv).pipe(Effect.provide(nodeContextLayer))); -} From 78ef02cf54d7671cde4febac08bc11d558db3611 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:33:40 -0500 Subject: [PATCH 06/14] fix monorepo ci regressions --- packages/cli/package.json | 2 +- packages/cli/src/cli/init.ts | 14 ++++++++++---- packages/cli/src/core/resolveInitRequest.ts | 4 ---- packages/cli/src/core/types.ts | 2 +- packages/cli/tests/browser-apps.smoke.test.ts | 2 +- .../tests/init-post-init-generation-errors.test.ts | 2 +- .../cli/tests/init-run-init-regression.test.ts | 2 +- packages/cli/tests/integration.test.ts | 2 +- packages/fmodata/src/errors.ts | 7 +------ 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 2f2a8cf5..7d37ff7f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,7 +50,7 @@ "pub:beta": "NODE_ENV=production pnpm build && npm publish --tag beta --access public", "pub:next": "NODE_ENV=production pnpm build && npm publish --tag next --access public", "pub:release": "NODE_ENV=production pnpm build && npm publish --access public", - "test": "vitest run", + "test": "pnpm build && vitest run", "test:smoke": "PROOFKIT_RUN_SMOKE_TESTS=1 vitest run --config vitest.smoke.config.ts" }, "dependencies": { diff --git a/packages/cli/src/cli/init.ts b/packages/cli/src/cli/init.ts index 4b8cc21c..b94c1405 100644 --- a/packages/cli/src/cli/init.ts +++ b/packages/cli/src/cli/init.ts @@ -148,6 +148,12 @@ function getErrorMessage(error: unknown): string { 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)); @@ -165,25 +171,25 @@ export function createPostInitGenerationError({ const rootError = error instanceof Error ? error : new Error(getErrorMessage(error)); if (appType === "browser" && isMissingTypegenCommandError(error)) { - return new 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 `proofkit typegen` later after FileMaker setup is complete.", ].join("\n"), - { cause: rootError }, + rootError, ); } - return new Error( + return createErrorWithCause( [ "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 }, + rootError, ); } diff --git a/packages/cli/src/core/resolveInitRequest.ts b/packages/cli/src/core/resolveInitRequest.ts index 8154452e..375d362b 100644 --- a/packages/cli/src/core/resolveInitRequest.ts +++ b/packages/cli/src/core/resolveInitRequest.ts @@ -423,10 +423,6 @@ export const resolveInitRequest = (name?: string, rawFlags?: CliFlags) => ).pipe(Effect.map((value) => value as DataSourceType)); } - const hasExplicitFileMakerInputs = Boolean( - flags.server || flags.adminApiKey || flags.dataApiKey || flags.fileName || flags.layoutName || flags.schemaName, - ); - if (nonInteractive && !flags.dataSource && hasExplicitFileMakerInputs) { return yield* Effect.fail(new Error("FileMaker flags require --data-source filemaker in non-interactive mode.")); } diff --git a/packages/cli/src/core/types.ts b/packages/cli/src/core/types.ts index a027bc88..e16810cf 100644 --- a/packages/cli/src/core/types.ts +++ b/packages/cli/src/core/types.ts @@ -107,7 +107,7 @@ export interface InitPlan { packageManager?: string; proofkitMetadata: { initVersion: string; - scaffoldPackage: "@proofkit/new"; + scaffoldPackage: "@proofkit/cli"; }; dependencies: Record; devDependencies: Record; diff --git a/packages/cli/tests/browser-apps.smoke.test.ts b/packages/cli/tests/browser-apps.smoke.test.ts index ef7d1c3a..bee9a231 100644 --- a/packages/cli/tests/browser-apps.smoke.test.ts +++ b/packages/cli/tests/browser-apps.smoke.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { beforeEach, describe, expect, it } from "vitest"; import { z } from "zod/v4"; -import { verifySmokeProjectBuilds } from "./test-utils"; +import { verifySmokeProjectBuilds } from "./test-utils.js"; describe("External integration smoke tests (non-interactive CLI)", () => { // Use root-level tmp directory for test outputs diff --git a/packages/cli/tests/init-post-init-generation-errors.test.ts b/packages/cli/tests/init-post-init-generation-errors.test.ts index f13e9dff..3927c96f 100644 --- a/packages/cli/tests/init-post-init-generation-errors.test.ts +++ b/packages/cli/tests/init-post-init-generation-errors.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { createPostInitGenerationError, isMissingTypegenCommandError } from "~/cli/init"; +import { createPostInitGenerationError, isMissingTypegenCommandError } from "~/cli/init.js"; vi.mock("@inquirer/prompts", () => ({ checkbox: vi.fn(), diff --git a/packages/cli/tests/init-run-init-regression.test.ts b/packages/cli/tests/init-run-init-regression.test.ts index 6422d97b..0313d427 100644 --- a/packages/cli/tests/init-run-init-regression.test.ts +++ b/packages/cli/tests/init-run-init-regression.test.ts @@ -137,7 +137,7 @@ vi.mock("~/cli/utils.js", () => ({ abortIfCancel: vi.fn((value: unknown) => value), })); -import { runInit } from "~/cli/init"; +import { runInit } from "~/cli/init.js"; const browserFilemakerFlags = { noGit: true, diff --git a/packages/cli/tests/integration.test.ts b/packages/cli/tests/integration.test.ts index c3694702..775461a1 100644 --- a/packages/cli/tests/integration.test.ts +++ b/packages/cli/tests/integration.test.ts @@ -197,7 +197,7 @@ describe("integration scaffold generation", () => { expect(packageJson.devDependencies.ultracite).toBe("7.0.8"); expect(agentsFile).toContain("Use the ProofKit docs as the primary reference"); expect(agentsFile).toContain("npx @tanstack/intent@latest install"); - expect(claudeFile?.trim()).toBe("AGENTS.md"); + expect(claudeFile).toBe(agentsFile); expect(launchConfig).toContain('"runtimeExecutable": "pnpm"'); expect(routerFile).toContain("createHashHistory"); expect(mainFile).toContain("QueryClientProvider"); diff --git a/packages/fmodata/src/errors.ts b/packages/fmodata/src/errors.ts index 987097e6..9a04f0fe 100644 --- a/packages/fmodata/src/errors.ts +++ b/packages/fmodata/src/errors.ts @@ -112,12 +112,7 @@ export class ValidationError extends FMODataError { cause?: Error["cause"]; }, ) { - const cause = options?.cause; - if (cause === undefined) { - super(message); - } else { - super(message, { cause }); - } + super(message, options?.cause === undefined ? undefined : { cause: options.cause }); this.field = options?.field; this.issues = issues; this.value = options?.value; From 972bc5cc8b46b46f094b7efd6921b54963e2b380 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:34:40 -0500 Subject: [PATCH 07/14] Remove cli-old entries from pnpm lockfile --- pnpm-lock.yaml | 221 ------------------------------------------------- 1 file changed, 221 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1990d2f1..5702896e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -475,214 +475,6 @@ importers: specifier: ^4.3.5 version: 4.3.5 - packages/cli-old: - dependencies: - '@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 - '@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/glob': - specifier: ^8.1.0 - version: 8.1.0 - 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 - 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 - open: - specifier: ^10.2.0 - version: 10.2.0 - ora: - specifier: 6.3.1 - version: 6.3.1 - randomstring: - specifier: ^1.3.1 - version: 1.3.1 - 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) - sort-package-json: - specifier: ^2.15.1 - version: 2.15.1 - ts-morph: - specifier: ^26.0.0 - version: 26.0.0 - 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)) - '@biomejs/biome': - specifier: 2.3.11 - version: 2.3.11 - '@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/registry': - specifier: workspace:* - version: link:../registry - '@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.6) - '@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(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(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/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 - '@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(@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)) - 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)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/react@19.2.7)(better-sqlite3@11.10.0)(kysely@0.28.12)(mysql2@3.16.0)(postgres@3.4.8)(react@19.2.3) - mysql2: - specifier: ^3.16.0 - version: 3.16.0 - next: - specifier: 16.1.1 - version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - next-auth: - specifier: ^4.24.13 - version: 4.24.13(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - 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 - react: - specifier: 19.2.3 - version: 19.2.3 - react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) - superjson: - specifier: ^2.2.6 - version: 2.2.6 - tailwindcss: - specifier: ^4.1.18 - version: 4.1.18 - 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 - ultracite: - specifier: 7.0.8 - version: 7.0.8(effect@3.20.0)(typescript@5.9.3) - vitest: - specifier: ^4.0.17 - version: 4.0.17(@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) - zod: - specifier: ^4.3.5 - version: 4.3.6 - packages/create-proofkit: dependencies: execa: @@ -11825,12 +11617,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@t3-oss/env-core@0.10.1(typescript@5.9.3)(zod@4.3.6)': - dependencies: - zod: 4.3.6 - 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) @@ -11838,13 +11624,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@t3-oss/env-nextjs@0.10.1(typescript@5.9.3)(zod@4.3.6)': - dependencies: - '@t3-oss/env-core': 0.10.1(typescript@5.9.3)(zod@4.3.6) - zod: 4.3.6 - optionalDependencies: - typescript: 5.9.3 - '@tabler/icons-react@3.36.1(react@19.2.3)': dependencies: '@tabler/icons': 3.36.1 From d4c97ee2af4644155c98dc254f70aae410091fde Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:37:09 -0500 Subject: [PATCH 08/14] Simplify CLI failure rendering logic --- packages/cli/src/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 39277255..dda814a2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -369,10 +369,7 @@ function shouldShowDebugDetails(argv: readonly string[]) { function renderFailure(cause: Cause.Cause, showDebugDetails: boolean) { const failure = getOrUndefined(Cause.failureOption(cause)); - if (failure && !isValidationError(failure)) { - const error = Cause.squash(cause); - console.error(error instanceof Error ? error.message : String(error)); - } else if (!failure) { + if (!failure || !isValidationError(failure)) { const error = Cause.squash(cause); console.error(error instanceof Error ? error.message : String(error)); } From 09ddbf9b029065ef5034eec0a1f3fc6f1babc8fc Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:39:54 -0500 Subject: [PATCH 09/14] Simplify CLI renderFailure validation guard --- packages/cli/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index dda814a2..2f4c98bb 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -369,7 +369,7 @@ function shouldShowDebugDetails(argv: readonly string[]) { function renderFailure(cause: Cause.Cause, showDebugDetails: boolean) { const failure = getOrUndefined(Cause.failureOption(cause)); - if (!failure || !isValidationError(failure)) { + if (!(failure && isValidationError(failure))) { const error = Cause.squash(cause); console.error(error instanceof Error ? error.message : String(error)); } From 422a9fa2a580e384cf4474f3dea757cf17dfe27e Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:46:22 -0500 Subject: [PATCH 10/14] Fix CLI project file traversal errors --- packages/cli/src/utils/projectFiles.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/utils/projectFiles.ts b/packages/cli/src/utils/projectFiles.ts index f70dd1e8..79bdbf5e 100644 --- a/packages/cli/src/utils/projectFiles.ts +++ b/packages/cli/src/utils/projectFiles.ts @@ -77,7 +77,21 @@ export async function replaceTextInFiles( const entries = await fs.readdir(rootDir); for (const entry of entries) { const fullPath = path.join(rootDir, entry); - const childEntries = await fs.readdir(fullPath).catch(() => undefined); + 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; From 26f84dd9543df2e016a43db544a58cc0939016ad Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:56:06 -0500 Subject: [PATCH 11/14] Fix webviewer MCP setup fallback messaging --- packages/cli/src/core/resolveInitRequest.ts | 103 ++++++++++++-------- packages/cli/tests/resolve-init.test.ts | 79 ++++++++++++++- packages/cli/tests/test-layer.ts | 65 +++++++++--- 3 files changed, 193 insertions(+), 54 deletions(-) diff --git a/packages/cli/src/core/resolveInitRequest.ts b/packages/cli/src/core/resolveInitRequest.ts index 375d362b..aea6e803 100644 --- a/packages/cli/src/core/resolveInitRequest.ts +++ b/packages/cli/src/core/resolveInitRequest.ts @@ -282,49 +282,72 @@ async function resolveFileMakerInputs({ validateLayoutInputs(flags); if (appType === "webviewer" && !flags.server) { - const localFmHttp = await fileMakerService.detectLocalFmHttp(); - if (localFmHttp.healthy && localFmHttp.connectedFiles[0]) { - return { - fileMaker: { - mode: "local-fm-http", - dataSourceName: "filemaker", - envNames: createDataSourceEnvNames("filemaker"), - fmHttpBaseUrl: localFmHttp.baseUrl, - fileName: localFmHttp.connectedFiles[0], - layoutName: flags.layoutName, - schemaName: flags.schemaName, - } satisfies FileMakerInputs, - skipFileMakerSetup: false, - }; - } + while (true) { + const localFmHttp = await fileMakerService.detectLocalFmHttp(); + if (localFmHttp.healthy && localFmHttp.connectedFiles[0]) { + return { + fileMaker: { + mode: "local-fm-http", + dataSourceName: "filemaker", + envNames: createDataSourceEnvNames("filemaker"), + fmHttpBaseUrl: localFmHttp.baseUrl, + fileName: localFmHttp.connectedFiles[0], + layoutName: flags.layoutName, + schemaName: flags.schemaName, + } satisfies FileMakerInputs, + skipFileMakerSetup: false, + }; + } - if (nonInteractive) { - throw new Error( - "No local FM HTTP connection was detected and no FileMaker server was provided. Start FM HTTP locally or rerun with --server.", - ); - } + if (nonInteractive) { + if (localFmHttp.healthy) { + throw new Error( + "ProofKit MCP Server was detected, but no FileMaker files are open. Open a file in FileMaker and rerun, or pass --server.", + ); + } - const fallbackAction = await prompt.select({ - message: "Local FM HTTP was not detected. How would you like to continue?", - options: [ - { - 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", - }, - ], - }); + throw new Error( + "ProofKit MCP Server was not detected and no FileMaker server was provided. Start the ProofKit MCP Server locally or rerun with --server.", + ); + } + + const fallbackAction = await prompt.select({ + message: localFmHttp.healthy + ? "I noticed you have the ProofKit MCP Server installed, but no files are open. How would you like to continue?" + : "ProofKit MCP Server was not detected. How would you like to continue?", + options: [ + { + value: "retry", + label: "Try again", + hint: localFmHttp.healthy + ? "Open a FileMaker file, then retry detection" + : "Retry ProofKit MCP Server 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, + }; + } - if (fallbackAction === "skip") { - return { - fileMaker: undefined, - skipFileMakerSetup: true, - }; + break; } } diff --git a/packages/cli/tests/resolve-init.test.ts b/packages/cli/tests/resolve-init.test.ts index 642fb480..aed69a01 100644 --- a/packages/cli/tests/resolve-init.test.ts +++ b/packages/cli/tests/resolve-init.test.ts @@ -1,7 +1,7 @@ import { Effect } from "effect"; import { describe, expect, it } from "vitest"; import { resolveInitRequest } from "~/core/resolveInitRequest.js"; -import { makeTestLayer } from "./test-layer.js"; +import { makeTestLayer, type PromptTranscript } from "./test-layer.js"; describe("resolveInitRequest", () => { it("fails for missing project name in non-interactive mode", async () => { @@ -172,4 +172,81 @@ describe("resolveInitRequest", () => { fileName: "LocalFile.fmp12", }); }); + + it("prompts to retry when Proofkit MCP is running but no FileMaker file is 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: { + select: ["skip"], + }, + promptTranscript, + fileMaker: { + localFmHttp: { + healthy: true, + connectedFiles: [], + }, + }, + }), + ), + ); + + expect(request.fileMaker).toBeUndefined(); + expect(request.skipFileMakerSetup).toBe(true); + expect(promptTranscript.select).toContainEqual({ + message: "I noticed you have the ProofKit MCP Server installed, but no files are open. How would you like to continue?", + options: ["retry", "hosted", "skip"], + }); + }); + + it("fails with a specific non-interactive error when Proofkit MCP is running but no FileMaker file is open", async () => { + await expect( + 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: { + localFmHttp: { + healthy: true, + connectedFiles: [], + }, + }, + }), + ), + ), + ).rejects.toThrow( + "ProofKit MCP Server was detected, but no FileMaker files are open. Open a file in FileMaker and rerun, or pass --server.", + ); + }); }); diff --git a/packages/cli/tests/test-layer.ts b/packages/cli/tests/test-layer.ts index acee2768..c0b19010 100644 --- a/packages/cli/tests/test-layer.ts +++ b/packages/cli/tests/test-layer.ts @@ -39,12 +39,25 @@ export interface ConsoleTranscript { 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; + promptTranscript?: PromptTranscript; tracker?: { commands: string[]; gitInits: number; @@ -78,40 +91,66 @@ export function makeTestLayer(options: { packageManager: options.packageManager, }), Layer.succeed(PromptService, { - text: ({ defaultValue }: { defaultValue?: string }) => { + text: ({ message, defaultValue }: { message: string; defaultValue?: string }) => { + options.promptTranscript?.text.push(message); const next = promptScript.text.shift(); return Promise.resolve(next ?? defaultValue ?? "value"); }, - password: () => Promise.resolve(promptScript.password.shift() ?? "password"), - select: ({ options }: { options: { value: T }[] }) => { + password: ({ message }: { message: string }) => { + options.promptTranscript?.password.push(message); + return Promise.resolve(promptScript.password.shift() ?? "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) { - const match = options.find((option) => option.value === next); + const match = selectOptions.find((option) => option.value === next); if (match) { return Promise.resolve(match.value); } } - return Promise.resolve(options[0]?.value ?? ("" as T)); + return Promise.resolve(selectOptions[0]?.value ?? ("" as T)); }, - searchSelect: ({ options }: { options: { value: T }[] }) => { + searchSelect: ({ + message, + options: searchOptions, + }: { + message: string; + options: { value: T }[]; + }) => { + options.promptTranscript?.searchSelect.push(message); const next = promptScript.searchSelect.shift(); if (next) { - const match = options.find((option) => option.value === next); + const match = searchOptions.find((option) => option.value === next); if (match) { return Promise.resolve(match.value); } } - return Promise.resolve(options[0]?.value ?? ("" as T)); + return Promise.resolve(searchOptions[0]?.value ?? ("" as T)); }, - multiSearchSelect: ({ options }: { options: { value: 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 => options.some((option) => option.value === value))); + return Promise.resolve( + next.filter((value): value is T => searchOptions.some((option) => option.value === value)), + ); } - return Promise.resolve(options.slice(0, 1).map((option) => option.value)); + return Promise.resolve(searchOptions.slice(0, 1).map((option) => option.value)); + }, + confirm: async ({ message, initialValue }: { message: string; initialValue?: boolean }) => { + options.promptTranscript?.confirm.push(message); + return promptScript.confirm.shift() ?? initialValue ?? false; }, - confirm: async ({ initialValue }: { initialValue?: boolean }) => - promptScript.confirm.shift() ?? initialValue ?? false, }), Layer.succeed(ConsoleService, { info: (message: string) => { From d9d0a500c42801698b4d95c65f22208545982876 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:19:18 -0500 Subject: [PATCH 12/14] Fix CLI lint violations --- packages/cli/src/utils/projectFiles.ts | 5 +---- packages/cli/tests/resolve-init.test.ts | 3 ++- packages/cli/tests/test-layer.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/utils/projectFiles.ts b/packages/cli/src/utils/projectFiles.ts index 79bdbf5e..8847fbb2 100644 --- a/packages/cli/src/utils/projectFiles.ts +++ b/packages/cli/src/utils/projectFiles.ts @@ -79,10 +79,7 @@ export async function replaceTextInFiles( 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" + typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : undefined; diff --git a/packages/cli/tests/resolve-init.test.ts b/packages/cli/tests/resolve-init.test.ts index aed69a01..8830d7f2 100644 --- a/packages/cli/tests/resolve-init.test.ts +++ b/packages/cli/tests/resolve-init.test.ts @@ -215,7 +215,8 @@ describe("resolveInitRequest", () => { expect(request.fileMaker).toBeUndefined(); expect(request.skipFileMakerSetup).toBe(true); expect(promptTranscript.select).toContainEqual({ - message: "I noticed you have the ProofKit MCP Server installed, but no files are open. How would you like to continue?", + message: + "I noticed you have the ProofKit MCP Server installed, but no files are open. How would you like to continue?", options: ["retry", "hosted", "skip"], }); }); diff --git a/packages/cli/tests/test-layer.ts b/packages/cli/tests/test-layer.ts index c0b19010..138c0649 100644 --- a/packages/cli/tests/test-layer.ts +++ b/packages/cli/tests/test-layer.ts @@ -147,7 +147,7 @@ export function makeTestLayer(options: { } return Promise.resolve(searchOptions.slice(0, 1).map((option) => option.value)); }, - confirm: async ({ message, initialValue }: { message: string; initialValue?: boolean }) => { + confirm: ({ message, initialValue }: { message: string; initialValue?: boolean }) => { options.promptTranscript?.confirm.push(message); return promptScript.confirm.shift() ?? initialValue ?? false; }, From 20ba49bcbde5bb790420c924f7823476978622ef Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:23:10 -0500 Subject: [PATCH 13/14] Require root CI before commits and fix test-layer confirm promise --- AGENTS.md | 26 +------------------------- packages/cli/tests/test-layer.ts | 2 +- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 393b7ca9..425a9de0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,25 +1 @@ -# Agent Instructions - -## Landing the Plane (Session Completion) - -**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. - -**MANDATORY WORKFLOW:** - -1. **File issues for remaining work** - Create issues for anything that needs follow-up -2. **Run quality gates** (if code changed) - Tests, linters, builds -3. **PUSH TO REMOTE** - This is MANDATORY: - ```bash - git pull --rebase - git push - git status # MUST show "up to date with origin" - ``` -4. **Clean up** - Clear stashes, prune remote branches -5. **Verify** - All changes committed AND pushed -6. **Hand off** - Provide context for next session - -**CRITICAL RULES:** -- Work is NOT complete until `git push` succeeds -- NEVER stop before pushing - that leaves work stranded locally -- NEVER say "ready to push when you are" - YOU must push -- If push fails, resolve and retry until it succeeds +Always run `pnpm run ci` from the root of the repo after completing a task and before committing. \ No newline at end of file diff --git a/packages/cli/tests/test-layer.ts b/packages/cli/tests/test-layer.ts index 138c0649..461fedaf 100644 --- a/packages/cli/tests/test-layer.ts +++ b/packages/cli/tests/test-layer.ts @@ -149,7 +149,7 @@ export function makeTestLayer(options: { }, confirm: ({ message, initialValue }: { message: string; initialValue?: boolean }) => { options.promptTranscript?.confirm.push(message); - return promptScript.confirm.shift() ?? initialValue ?? false; + return Promise.resolve(promptScript.confirm.shift() ?? initialValue ?? false); }, }), Layer.succeed(ConsoleService, { From a5c9ea6a9aa4d12f4e91432fd2861411de041663 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:25:22 -0500 Subject: [PATCH 14/14] Stabilize CLI tests by removing rebuild races --- packages/cli/tests/cli.test.ts | 13 -------- .../init-non-interactive-failures.test.ts | 30 +++---------------- .../cli/tests/init-scaffold-contract.test.ts | 8 ----- packages/cli/tests/webviewer-apps.test.ts | 9 +----- 4 files changed, 5 insertions(+), 55 deletions(-) diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index c6b51249..1b03d4d5 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -9,17 +9,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const packageDir = path.join(__dirname, ".."); const distEntry = path.join(packageDir, "dist/index.js"); -function buildCli() { - execFileSync("pnpm", ["build"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); -} - describe("proofkit CLI", () => { it("shows kebab-case init flags in help", () => { - buildCli(); const output = execFileSync("node", [distEntry, "init", "--help"], { cwd: packageDir, stdio: "pipe", @@ -34,7 +25,6 @@ describe("proofkit CLI", () => { }); it("prints the header and project command guidance when run inside a ProofKit project", async () => { - buildCli(); const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-project-")); await fs.writeJson(path.join(cwd, "proofkit.json"), { appType: "browser", @@ -57,7 +47,6 @@ describe("proofkit CLI", () => { }); it("fails with guidance when no command is used in non-interactive mode", () => { - buildCli(); const result = spawnSync("node", [distEntry, "--non-interactive"], { cwd: packageDir, stdio: "pipe", @@ -70,7 +59,6 @@ describe("proofkit CLI", () => { }); it("shows a clean invalid subcommand error by default", () => { - buildCli(); const result = spawnSync("node", [distEntry, "my-proofkit-app", "--force"], { cwd: packageDir, stdio: "pipe", @@ -88,7 +76,6 @@ describe("proofkit CLI", () => { }); it("shows internal error details when debug mode is enabled", () => { - buildCli(); const result = spawnSync("node", [distEntry, "--debug", "my-proofkit-app", "--force"], { cwd: packageDir, stdio: "pipe", diff --git a/packages/cli/tests/init-non-interactive-failures.test.ts b/packages/cli/tests/init-non-interactive-failures.test.ts index 330b3bf5..bc6656aa 100644 --- a/packages/cli/tests/init-non-interactive-failures.test.ts +++ b/packages/cli/tests/init-non-interactive-failures.test.ts @@ -21,7 +21,6 @@ function toText(value: string | Buffer | undefined) { } 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"); @@ -30,36 +29,15 @@ describe("Init Non-Interactive Failure Paths", () => { mkdirSync(testDir, { recursive: true }); }); - const rebuildCli = () => { - execFileSync("pnpm", ["build"], { - cwd: cliRoot, + const runInitCommand = (args: string[], cwd = testDir) => { + return execFileSync("node", [cliPath, "init", ...args], { + cwd, env: process.env, stdio: "pipe", + encoding: "utf-8", }); }; - 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); diff --git a/packages/cli/tests/init-scaffold-contract.test.ts b/packages/cli/tests/init-scaffold-contract.test.ts index 01145813..e4dd0790 100644 --- a/packages/cli/tests/init-scaffold-contract.test.ts +++ b/packages/cli/tests/init-scaffold-contract.test.ts @@ -37,14 +37,6 @@ const packageManagerPattern = /^(npm|pnpm|yarn|bun)@/; const ansiStylePrefixPattern = /^[0-9;]*m/; function runInit({ appType, projectName }: { appType: "browser" | "webviewer"; projectName: string }): string { - if (!existsSync(cliPath)) { - execFileSync("pnpm", ["build"], { - cwd: join(__dirname, ".."), - env: process.env, - stdio: "pipe", - }); - } - return execFileSync( "node", [ diff --git a/packages/cli/tests/webviewer-apps.test.ts b/packages/cli/tests/webviewer-apps.test.ts index f97c80c1..71b4bd19 100644 --- a/packages/cli/tests/webviewer-apps.test.ts +++ b/packages/cli/tests/webviewer-apps.test.ts @@ -1,4 +1,4 @@ -import { execFileSync, execSync } from "node:child_process"; +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"; @@ -12,13 +12,6 @@ describe("WebViewer CLI Tests", () => { const projectDir = join(testDir, projectName); beforeEach(() => { - if (!existsSync(cliPath)) { - execFileSync("pnpm", ["build"], { - cwd: join(__dirname, ".."), - env: process.env, - stdio: "pipe", - }); - } if (existsSync(projectDir)) { rmSync(projectDir, { recursive: true, force: true }); }